mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-21 17:22:55 -05:00
Add query history telemetry (#20722)
* Add query history telemetry * Update property name * Only send on success * Update name
This commit is contained in:
@@ -209,6 +209,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/ads-extension-telemetry": "1.2.0",
|
||||
"vscode-nls": "^4.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DOUBLE_CLICK_ACTION_CONFIG_SECTION, ITEM_SELECTED_COMMAND_ID, QUERY_HIS
|
||||
import { QueryHistoryItem } from './queryHistoryItem';
|
||||
import { QueryHistoryProvider, setLoadingContext } from './queryHistoryProvider';
|
||||
import { promises as fs } from 'fs';
|
||||
import { TelemetryActions, TelemetryReporter, TelemetryViews } from './telemetry';
|
||||
|
||||
let lastSelectedItem: { item: QueryHistoryItem | undefined, time: number | undefined } = {
|
||||
item: undefined,
|
||||
@@ -26,6 +27,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
|
||||
await fs.mkdir(storageUri.fsPath);
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistory, 'CreatingStorageFolder');
|
||||
console.error(`Error creating query history global storage folder ${context.globalStorageUri.fsPath}. ${err}`);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +46,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
|
||||
const clickTime = new Date().getTime();
|
||||
if (lastSelectedItem.item === selectedItem && lastSelectedItem.time && (clickTime - lastSelectedItem.time) < DOUBLE_CLICK_TIMEOUT_MS) {
|
||||
const doubleClickAction = vscode.workspace.getConfiguration(QUERY_HISTORY_CONFIG_SECTION).get<string>(DOUBLE_CLICK_ACTION_CONFIG_SECTION);
|
||||
TelemetryReporter.sendActionEvent(TelemetryViews.QueryHistory, TelemetryActions.DoubleClick, doubleClickAction);
|
||||
switch (doubleClickAction) {
|
||||
case 'run':
|
||||
await runQuery(selectedItem);
|
||||
@@ -91,21 +94,37 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
|
||||
}
|
||||
|
||||
async function openQuery(item: QueryHistoryItem): Promise<void> {
|
||||
await azdata.queryeditor.openQueryDocument(
|
||||
{
|
||||
content: item.queryText
|
||||
}, item.connectionProfile?.providerId);
|
||||
try {
|
||||
await azdata.queryeditor.openQueryDocument(
|
||||
{
|
||||
content: item.queryText
|
||||
}, item.connectionProfile?.providerId);
|
||||
} catch (err) {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistory, 'OpenQuery');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function runQuery(item: QueryHistoryItem): Promise<void> {
|
||||
const doc = await azdata.queryeditor.openQueryDocument(
|
||||
{
|
||||
content: item.queryText
|
||||
}, item.connectionProfile?.providerId);
|
||||
if (item.connectionProfile) {
|
||||
await doc.connect(item.connectionProfile);
|
||||
} else {
|
||||
await azdata.queryeditor.connect(doc.uri, '');
|
||||
let step = 'OpenDoc';
|
||||
try {
|
||||
const doc = await azdata.queryeditor.openQueryDocument(
|
||||
{
|
||||
content: item.queryText
|
||||
}, item.connectionProfile?.providerId);
|
||||
if (item.connectionProfile) {
|
||||
step = 'ConnectWithProfile';
|
||||
await doc.connect(item.connectionProfile);
|
||||
} else {
|
||||
step = 'ConnectWithoutProfile';
|
||||
await azdata.queryeditor.connect(doc.uri, '');
|
||||
}
|
||||
step = 'Run';
|
||||
azdata.queryeditor.runQuery(doc.uri);
|
||||
} catch (err) {
|
||||
TelemetryReporter.createErrorEvent(TelemetryViews.QueryHistory, 'RunQuery')
|
||||
.withAdditionalProperties({ step })
|
||||
.send();
|
||||
}
|
||||
azdata.queryeditor.runQuery(doc.uri);
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import * as loc from './localizedConstants';
|
||||
import { sendSettingChangedEvent, TelemetryActions, TelemetryReporter, TelemetryViews, TimedAction } from './telemetry';
|
||||
|
||||
const STORAGE_IV_KEY = 'queryHistory.storage-iv';
|
||||
const STORAGE_KEY_KEY = 'queryHistory.storage-key';
|
||||
@@ -50,7 +51,8 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
constructor(private _context: vscode.ExtensionContext, storageUri: vscode.Uri) {
|
||||
this._historyStorageFile = path.join(storageUri.fsPath, HISTORY_STORAGE_FILE_NAME);
|
||||
// Kick off initialization but then continue on since that may take a while and we don't want to block extension activation
|
||||
this._initPromise = this.initialize();
|
||||
const initializeAction = new TimedAction(TelemetryViews.QueryHistoryProvider, TelemetryActions.Initialize);
|
||||
this._initPromise = this.initialize().then(() => initializeAction.send());
|
||||
this._disposables.push(vscode.workspace.onDidChangeConfiguration(async e => {
|
||||
if (e.affectsConfiguration(QUERY_HISTORY_CONFIG_SECTION) || e.affectsConfiguration(MAX_ENTRIES_CONFIG_SECTION)) {
|
||||
await this.updateConfigurationValues();
|
||||
@@ -82,6 +84,7 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
// We need to compare URIs, but the event Uri comes in as string so while it should be in the same format as
|
||||
// the textDocument uri.toString() we parse it into a vscode.Uri first to be absolutely sure.
|
||||
if (textEditor?.document.uri.toString() !== vscode.Uri.parse(document.uri).toString()) {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'UriMismatch');
|
||||
// If we couldn't find the document then we can't get the text so just log the error and move on
|
||||
console.error(`Active text editor ${textEditor?.document.uri} does not match URI ${document.uri} for query event`);
|
||||
return;
|
||||
@@ -114,6 +117,7 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error getting persistance storage IV: ${err}`);
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'InitializingIV');
|
||||
// An IV is required to read/write the encrypted file so if we can't get it then just fail early
|
||||
return;
|
||||
}
|
||||
@@ -129,21 +133,31 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error getting persistance storage key: ${err}`);
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'InitializingKey');
|
||||
// A key is required to read/write the encrypted file so if we can't get it then just fail early
|
||||
return;
|
||||
}
|
||||
|
||||
this.writeHistoryFileWorker = (): void => {
|
||||
if (this._persistHistory) {
|
||||
|
||||
try {
|
||||
// We store the history entries in an encrypted file because they may contain sensitive information
|
||||
// such as passwords (even in the query text itself)
|
||||
const cipher = crypto.createCipheriv(STORAGE_ENCRYPTION_ALGORITHM, key!, iv!);
|
||||
const stringifiedItems = JSON.stringify(this._queryHistoryItems);
|
||||
const encryptedText = Buffer.concat([cipher.update(Buffer.from(stringifiedItems)), cipher.final()]);
|
||||
const writeStorageFileAction = new TimedAction(TelemetryViews.QueryHistoryProvider, TelemetryActions.WriteStorageFile,
|
||||
{},
|
||||
{
|
||||
ItemCount: this._queryHistoryItems.length,
|
||||
ItemLengthChars: stringifiedItems.length
|
||||
});
|
||||
// Use sync here so that we can write this out when the object is disposed
|
||||
fs.writeFileSync(this._historyStorageFile, encryptedText);
|
||||
writeStorageFileAction.send();
|
||||
} catch (err) {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'WriteStorageFile');
|
||||
console.error(`Error writing query history to disk: ${err}`);
|
||||
}
|
||||
|
||||
@@ -155,9 +169,12 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const readStorageFileAction = new TimedAction(TelemetryViews.QueryHistoryProvider, TelemetryActions.ReadStorageFile);
|
||||
// Read and decrypt any previous history items
|
||||
const encryptedItems = await fs.promises.readFile(this._historyStorageFile);
|
||||
readStorageFileAction.send();
|
||||
const decipher = crypto.createDecipheriv(STORAGE_ENCRYPTION_ALGORITHM, key, iv);
|
||||
const result = Buffer.concat([decipher.update(encryptedItems), decipher.final()]).toString();
|
||||
this._queryHistoryItems = JSON.parse(result);
|
||||
@@ -165,6 +182,7 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
} catch (err) {
|
||||
// Ignore ENOENT errors, those are expected if the storage file doesn't exist (on first run or if results aren't being persisted)
|
||||
if (err.code !== 'ENOENT') {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'ReadStorageFile');
|
||||
console.error(`Error deserializing stored history items: ${err}`);
|
||||
void vscode.window.showWarningMessage(loc.errorLoading(err));
|
||||
// Rename the file to avoid attempting to load a potentially corrupted or unreadable file every time we start up, we'll make
|
||||
@@ -173,6 +191,7 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
const bakPath = path.join(path.dirname(this._historyStorageFile), `${HISTORY_STORAGE_FILE_NAME}.bak`);
|
||||
await fs.promises.rename(this._historyStorageFile, bakPath);
|
||||
} catch (err) {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'MovingBadStorageFile');
|
||||
console.error(`Error moving corrupted history file: ${err}`);
|
||||
}
|
||||
}
|
||||
@@ -231,9 +250,21 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
|
||||
private async updateConfigurationValues(): Promise<void> {
|
||||
const configSection = vscode.workspace.getConfiguration(QUERY_HISTORY_CONFIG_SECTION);
|
||||
this._captureEnabled = configSection.get(CAPTURE_ENABLED_CONFIG_SECTION, DEFAULT_CAPTURE_ENABLED);
|
||||
this._persistHistory = configSection.get(PERSIST_HISTORY_CONFIG_SECTION, DEFAULT_PERSIST_HISTORY);
|
||||
this._maxEntries = configSection.get(MAX_ENTRIES_CONFIG_SECTION, DEFAULT_MAX_ENTRIES);
|
||||
const newCaptureEnabled = configSection.get(CAPTURE_ENABLED_CONFIG_SECTION, DEFAULT_CAPTURE_ENABLED);
|
||||
if (this._captureEnabled !== newCaptureEnabled) {
|
||||
sendSettingChangedEvent('CaptureEnabled', String(this._captureEnabled), String(newCaptureEnabled));
|
||||
this._captureEnabled = newCaptureEnabled;
|
||||
}
|
||||
const newPersistHistory = configSection.get(PERSIST_HISTORY_CONFIG_SECTION, DEFAULT_PERSIST_HISTORY);
|
||||
if (this._persistHistory !== newPersistHistory) {
|
||||
sendSettingChangedEvent('PersistHistory', String(this._persistHistory), String(newPersistHistory));
|
||||
this._persistHistory = newPersistHistory;
|
||||
}
|
||||
const newMaxEntries = configSection.get(MAX_ENTRIES_CONFIG_SECTION, DEFAULT_MAX_ENTRIES);
|
||||
if (this._maxEntries !== newMaxEntries) {
|
||||
sendSettingChangedEvent('MaxEntries', String(this._maxEntries), String(newMaxEntries));
|
||||
this._maxEntries = newMaxEntries;
|
||||
}
|
||||
this.trimExtraEntries();
|
||||
if (!this._persistHistory) {
|
||||
// We're not persisting history so we can immediately set loading to false to immediately
|
||||
@@ -246,6 +277,7 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
} catch (err) {
|
||||
// Ignore ENOENT errors, those are expected if the storage file doesn't exist (on first run or if results aren't being persisted)
|
||||
if (err.code !== 'ENOENT') {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'CleaningUpStorageFile');
|
||||
// Best effort, we don't want other things to fail if we can't delete the file for some reason
|
||||
console.error(`Error cleaning up query history storage: ${this._historyStorageFile}. ${err}`);
|
||||
}
|
||||
|
||||
87
extensions/query-history/src/telemetry.ts
Normal file
87
extensions/query-history/src/telemetry.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import AdsTelemetryReporter, { TelemetryEventMeasures, TelemetryEventProperties } from '@microsoft/ads-extension-telemetry';
|
||||
|
||||
export interface PackageInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
aiKey: string;
|
||||
}
|
||||
|
||||
const packageInfo = require('../package.json') as PackageInfo;
|
||||
|
||||
export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
|
||||
|
||||
/**
|
||||
* A helper class to send an Action event with a duration, timer starts on construction and ends when send() is called.
|
||||
*/
|
||||
export class TimedAction {
|
||||
/**
|
||||
* Additional properties to send along with the final message once send is called.
|
||||
*/
|
||||
public readonly additionalProperties: TelemetryEventProperties = {};
|
||||
/**
|
||||
* Additional measures to send along with the final message once send is called.
|
||||
*/
|
||||
public readonly additionalMeasures: TelemetryEventMeasures = {};
|
||||
|
||||
private _start: number = Date.now();
|
||||
|
||||
/**
|
||||
* Creates a new TimedAction and sets the start time to Date.now().
|
||||
* @param _view The view this action originates from
|
||||
* @param _action The name of the action
|
||||
* @param properties Additional properties to send along with the final message once send is called
|
||||
* @param measures Additional measures to send along with the final message once send is called
|
||||
*/
|
||||
constructor(private _view: TelemetryViews, private _action: TelemetryActions, properties: TelemetryEventProperties = {}, measures: TelemetryEventMeasures = {}) {
|
||||
Object.assign(this.additionalProperties, properties);
|
||||
Object.assign(this.additionalMeasures, measures);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the event with the duration being the difference between when this TimedAction was created and now.
|
||||
* @param properties Additional properties to send along with the event
|
||||
* @param measures Additional measures to send along with the event
|
||||
*/
|
||||
public send(properties: TelemetryEventProperties = {}, measures: TelemetryEventMeasures = {}): void {
|
||||
Object.assign(this.additionalProperties, properties);
|
||||
Object.assign(this.additionalMeasures, measures);
|
||||
TelemetryReporter.createActionEvent(this._view, this._action, undefined, undefined, Date.now() - this._start)
|
||||
.withAdditionalProperties(this.additionalProperties)
|
||||
.withAdditionalMeasurements(this.additionalMeasures)
|
||||
.send();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an event indicating that a setting changed along with the new and old values. Core has a setting changed
|
||||
* event already, but that doesn't capture the values so we make our own here.
|
||||
* @param setting The name of the setting
|
||||
* @param oldValue The original value
|
||||
* @param newValue The new value
|
||||
*/
|
||||
export function sendSettingChangedEvent(setting: string, oldValue: string, newValue: string): void {
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.QueryHistoryProvider, TelemetryActions.SettingChanged, setting)
|
||||
.withAdditionalProperties({
|
||||
oldValue,
|
||||
newValue
|
||||
})
|
||||
.send();
|
||||
}
|
||||
|
||||
export enum TelemetryViews {
|
||||
QueryHistory = 'QueryHistory',
|
||||
QueryHistoryProvider = 'QueryHistoryProvider'
|
||||
}
|
||||
|
||||
export enum TelemetryActions {
|
||||
DoubleClick = 'DoubleClick',
|
||||
Initialize = 'Initialize',
|
||||
ReadStorageFile = 'ReadStorageFile',
|
||||
SettingChanged = 'SettingChanged',
|
||||
WriteStorageFile = 'WriteStorageFile'
|
||||
}
|
||||
@@ -228,6 +228,44 @@
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@microsoft/1ds-core-js@3.2.6", "@microsoft/1ds-core-js@^3.2.3":
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.6.tgz#8a77909f89f991aa2f0b4ae8825c75042962e68e"
|
||||
integrity sha512-6OpppYCEA+rXjcs2w0KnWji3Y6ZDx0wykY7ZL3QF68NS323C45GHSpkDpVRT/lDU6Xbau/PvQm2zTYAzLcperA==
|
||||
dependencies:
|
||||
"@microsoft/applicationinsights-core-js" "2.8.6"
|
||||
"@microsoft/applicationinsights-shims" "^2.0.1"
|
||||
"@microsoft/dynamicproto-js" "^1.1.6"
|
||||
|
||||
"@microsoft/1ds-post-js@^3.2.3":
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.6.tgz#cdfa74acfc3205c0a5b79925284d6d166aa43901"
|
||||
integrity sha512-Zdyl3FU6kU/a7TlVVSTBZg+hSECTT65iI99FsjMOx88HudyVyk9M/0lVbA+FVXvGaxzmtBW6Lw0qRHYp4tBMSA==
|
||||
dependencies:
|
||||
"@microsoft/1ds-core-js" "3.2.6"
|
||||
"@microsoft/applicationinsights-shims" "^2.0.1"
|
||||
"@microsoft/dynamicproto-js" "^1.1.6"
|
||||
|
||||
"@microsoft/ads-extension-telemetry@1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/ads-extension-telemetry/-/ads-extension-telemetry-1.2.0.tgz#f54e464ac887440727fe9862f8ff32be17aeab3a"
|
||||
integrity sha512-dEp+RVJYo4uebMLvBJqJF8IABufJRp+PWHZx+3xe6SgAC37oYhcwR/glExhp3Nj3A2v3vjso6YQ/Wd5TG27FPQ==
|
||||
dependencies:
|
||||
"@vscode/extension-telemetry" "^0.6.2"
|
||||
|
||||
"@microsoft/applicationinsights-core-js@2.8.6":
|
||||
version "2.8.6"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.6.tgz#4f0f9ad809aacfc96cb882139b69b3625519c51a"
|
||||
integrity sha512-rL+ceda1Y6HaHBe1vIbNT/f5JGuHiD5Ydq+DoAfu56o13wyJu4sao3QKaabgaIM59pPO+3BMeGsK8NNUGYaT3w==
|
||||
dependencies:
|
||||
"@microsoft/applicationinsights-shims" "2.0.1"
|
||||
"@microsoft/dynamicproto-js" "^1.1.6"
|
||||
|
||||
"@microsoft/applicationinsights-shims@2.0.1", "@microsoft/applicationinsights-shims@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.1.tgz#5d72fb7aaf4056c4fda54f9d7c93ccf8ca9bcbfd"
|
||||
integrity sha512-G0MXf6R6HndRbDy9BbEj0zrLeuhwt2nsXk2zKtF0TnYo39KgYqhYC2ayIzKPTm2KAE+xzD7rgyLdZnrcRvt9WQ==
|
||||
|
||||
"@microsoft/azdata-test@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/azdata-test/-/azdata-test-3.0.0.tgz#5cc4f416e464e6127dc333afaf983ebe49a66f37"
|
||||
@@ -240,6 +278,11 @@
|
||||
rimraf "^2.6.3"
|
||||
typemoq "^2.1.0"
|
||||
|
||||
"@microsoft/dynamicproto-js@^1.1.6":
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.6.tgz#6fe03468862861f5f88ac4c3959a652b3797f1bc"
|
||||
integrity sha512-D1Oivw1A4bIXhzBIy3/BBPn3p2On+kpO2NiYt9shICDK7L/w+cR6FFBUsBZ05l6iqzTeL+Jm8lAYn0g6G7DmDg==
|
||||
|
||||
"@microsoft/vscodetestcover@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/vscodetestcover/-/vscodetestcover-1.2.1.tgz#65f25132075a465a7a99688204486ee2b65ac07b"
|
||||
@@ -265,6 +308,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.52.tgz#2fd2dc6bfa185601b15457398d4ba1ef27f81251"
|
||||
integrity sha512-cfkwWw72849SNYp3Zx0IcIs25vABmFh73xicxhCkTcvtZQeIez15PpwQN8fY3RD7gv1Wrxlc9MEtfMORZDEsGw==
|
||||
|
||||
"@vscode/extension-telemetry@^0.6.2":
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.6.2.tgz#b86814ee680615730da94220c2b03ea9c3c14a8e"
|
||||
integrity sha512-yb/wxLuaaCRcBAZtDCjNYSisAXz3FWsSqAha5nhHcYxx2ZPdQdWuZqVXGKq0ZpHVndBWWtK6XqtpCN2/HB4S1w==
|
||||
dependencies:
|
||||
"@microsoft/1ds-core-js" "^3.2.3"
|
||||
"@microsoft/1ds-post-js" "^3.2.3"
|
||||
|
||||
agent-base@4, agent-base@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
|
||||
|
||||
Reference in New Issue
Block a user