From df9013d2e26886241733cc75b87bc2f204a2eb30 Mon Sep 17 00:00:00 2001 From: mustard Date: Fri, 9 Sep 2022 10:01:42 +0000 Subject: [PATCH 1/2] Add experiments using ConfigCat with PortsView --- extensions/gitpod-shared/src/features.ts | 8 ++- extensions/gitpod-web/package.json | 1 + extensions/gitpod-web/src/experiments.ts | 91 ++++++++++++++++++++++++ extensions/gitpod-web/src/extension.ts | 32 ++++++--- extensions/yarn.lock | 18 +++++ 5 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 extensions/gitpod-web/src/experiments.ts diff --git a/extensions/gitpod-shared/src/features.ts b/extensions/gitpod-shared/src/features.ts index 8e09ade52a5a4..0d96a89ad1048 100644 --- a/extensions/gitpod-shared/src/features.ts +++ b/extensions/gitpod-shared/src/features.ts @@ -8,6 +8,7 @@ require('reflect-metadata'); import { GitpodClient, GitpodServer, GitpodServiceImpl, WorkspaceInstanceUpdateListener } from '@gitpod/gitpod-protocol/lib/gitpod-service'; import { JsonRpcProxyFactory } from '@gitpod/gitpod-protocol/lib/messaging/proxy-factory'; import { NavigatorContext, User } from '@gitpod/gitpod-protocol/lib/protocol'; +import { Team } from '@gitpod/gitpod-protocol/lib/teams-projects-protocol'; import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error'; import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url'; import { ControlServiceClient } from '@gitpod/supervisor-api-grpc/lib/control_grpc_pb'; @@ -70,7 +71,7 @@ export class SupervisorConnection { } } -type UsedGitpodFunction = ['getWorkspace', 'openPort', 'stopWorkspace', 'setWorkspaceTimeout', 'getWorkspaceTimeout', 'getLoggedInUser', 'takeSnapshot', 'waitForSnapshot', 'controlAdmission', 'sendHeartBeat', 'trackEvent']; +type UsedGitpodFunction = ['getWorkspace', 'openPort', 'stopWorkspace', 'setWorkspaceTimeout', 'getWorkspaceTimeout', 'getLoggedInUser', 'takeSnapshot', 'waitForSnapshot', 'controlAdmission', 'sendHeartBeat', 'trackEvent', 'getTeams']; type Union = Tuple[number] | Union; export type GitpodConnection = Omit, 'server'> & { server: Pick>; @@ -93,6 +94,7 @@ export class GitpodExtensionContext implements vscode.ExtensionContext { readonly info: WorkspaceInfoResponse, readonly owner: Promise, readonly user: Promise, + readonly userTeams: Promise, readonly instanceListener: Promise, readonly workspaceOwned: Promise, readonly logger: Log, @@ -241,7 +243,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont const gitpodApi = workspaceInfo.getGitpodApi()!; const factory = new JsonRpcProxyFactory(); - const gitpodFunctions: UsedGitpodFunction = ['getWorkspace', 'openPort', 'stopWorkspace', 'setWorkspaceTimeout', 'getWorkspaceTimeout', 'getLoggedInUser', 'takeSnapshot', 'waitForSnapshot', 'controlAdmission', 'sendHeartBeat', 'trackEvent']; + const gitpodFunctions: UsedGitpodFunction = ['getWorkspace', 'openPort', 'stopWorkspace', 'setWorkspaceTimeout', 'getWorkspaceTimeout', 'getLoggedInUser', 'takeSnapshot', 'waitForSnapshot', 'controlAdmission', 'sendHeartBeat', 'trackEvent', 'getTeams']; const gitpodService: GitpodConnection = new GitpodServiceImpl(factory.createProxy()) as any; const gitpodScopes = new Set([ 'resource:workspace::' + workspaceId + '::get/update', @@ -302,6 +304,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont } return vscode.commands.executeCommand('gitpod.api.getLoggedInUser') as typeof pendingGetOwner; })(); + const pendingGetUserTeams = gitpodService.server.getTeams(); const pendingInstanceListener = gitpodService.listenToInstance(workspaceId); const pendingWorkspaceOwned = (async () => { const owner = await pendingGetOwner; @@ -325,6 +328,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont workspaceInfo, pendingGetOwner, pendingGetUser, + pendingGetUserTeams, pendingInstanceListener, pendingWorkspaceOwned, logger, diff --git a/extensions/gitpod-web/package.json b/extensions/gitpod-web/package.json index ad1f6194e95ae..0c51e619b1e04 100644 --- a/extensions/gitpod-web/package.json +++ b/extensions/gitpod-web/package.json @@ -562,6 +562,7 @@ "dependencies": { "@gitpod/gitpod-protocol": "main", "@gitpod/supervisor-api-grpc": "main", + "configcat-node": "^8.0.0", "gitpod-shared": "0.0.1", "node-fetch": "2.6.7", "uuid": "8.1.0", diff --git a/extensions/gitpod-web/src/experiments.ts b/extensions/gitpod-web/src/experiments.ts new file mode 100644 index 0000000000000..6b45dcb4a98d8 --- /dev/null +++ b/extensions/gitpod-web/src/experiments.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as configcat from 'configcat-node'; +import * as configcatcommon from 'configcat-common'; +import * as semver from 'semver'; +import Log from 'gitpod-shared/out/common/logger'; +import { URL } from 'url'; + +const EXPERTIMENTAL_SETTINGS = [ + 'gitpod.experimental.portsView.enabled', +]; + +export class ExperimentalSettings { + private configcatClient: configcatcommon.IConfigCatClient; + private extensionVersion: semver.SemVer; + + constructor(key: string, extensionVersion: string, private logger: Log, gitpodHost: string) { + this.configcatClient = configcat.createClientWithLazyLoad(key, { + baseUrl: new URL('/configcat', process.env['VSCODE_DEV'] ? 'https://gitpod-staging.com' : gitpodHost).href, + logger: { + debug(): void { }, + log(): void { }, + info(): void { }, + warn(message: string): void { logger.warn(`ConfigCat: ${message}`); }, + error(message: string): void { logger.error(`ConfigCat: ${message}`); } + }, + requestTimeoutMs: 1500, + cacheTimeToLiveSeconds: 60 + }); + this.extensionVersion = new semver.SemVer(extensionVersion); + } + + async get(key: string, userId?: string, custom?: { [key: string]: string }): Promise { + const config = vscode.workspace.getConfiguration('gitpod'); + const values = config.inspect(key.substring('gitpod.'.length)); + if (!values || !EXPERTIMENTAL_SETTINGS.includes(key)) { + this.logger.error(`Cannot get invalid experimental setting '${key}'`); + return values?.globalValue ?? values?.defaultValue; + } + if (this.isPreRelease()) { + // PreRelease versions always have experiments enabled by default + return values.globalValue ?? values.defaultValue; + } + if (values.globalValue !== undefined) { + // User setting have priority over configcat so return early + return values.globalValue; + } + + const user = userId ? new configcatcommon.User(userId, undefined, undefined, custom) : undefined; + const configcatKey = key.replace(/\./g, '_'); // '.' are not allowed in configcat + const experimentValue = (await this.configcatClient.getValueAsync(configcatKey, undefined, user)) as T | undefined; + + return experimentValue ?? values.defaultValue; + } + + async inspect(key: string, userId?: string, custom?: { [key: string]: string }): Promise<{ key: string; defaultValue?: T; globalValue?: T; experimentValue?: T } | undefined> { + const config = vscode.workspace.getConfiguration('gitpod'); + const values = config.inspect(key.substring('gitpod.'.length)); + if (!values || !EXPERTIMENTAL_SETTINGS.includes(key)) { + this.logger.error(`Cannot inspect invalid experimental setting '${key}'`); + return values; + } + + const user = userId ? new configcatcommon.User(userId, undefined, undefined, custom) : undefined; + const configcatKey = key.replace(/\./g, '_'); // '.' are not allowed in configcat + const experimentValue = (await this.configcatClient.getValueAsync(configcatKey, undefined, user)) as T | undefined; + + return { key, defaultValue: values.defaultValue, globalValue: values.globalValue, experimentValue }; + } + + forceRefreshAsync(): Promise { + return this.configcatClient.forceRefreshAsync(); + } + + private isPreRelease() { + return this.extensionVersion.minor % 2 === 1; + } + + dispose(): void { + this.configcatClient.dispose(); + } +} + +export function isUserOverrideSetting(key: string): boolean { + const config = vscode.workspace.getConfiguration('gitpod'); + const values = config.inspect(key.substring('gitpod.'.length)); + return values?.globalValue !== undefined; +} diff --git a/extensions/gitpod-web/src/extension.ts b/extensions/gitpod-web/src/extension.ts index 0886b6e4c29cf..2421257a9d656 100644 --- a/extensions/gitpod-web/src/extension.ts +++ b/extensions/gitpod-web/src/extension.ts @@ -25,6 +25,7 @@ import { getManifest } from './util/extensionManagmentUtill'; import { GitpodWorkspacePort, PortInfo, iconStatusMap } from './util/port'; import { ReleaseNotes } from './releaseNotes'; import { registerWelcomeWalkthroughContribution, WELCOME_WALKTROUGH_KEY } from './welcomeWalktrough'; +import { ExperimentalSettings, isUserOverrideSetting } from './experiments'; let gitpodContext: GitpodExtensionContext | undefined; export async function activate(context: vscode.ExtensionContext) { @@ -516,8 +517,16 @@ function getNonce() { interface PortItem { port: GitpodWorkspacePort; isWebview?: boolean } -function registerPorts(context: GitpodExtensionContext): void { - const isPortsViewExperimentEnable = vscode.workspace.getConfiguration('gitpod.experimental.portsView').get('enabled'); +async function registerPorts(context: GitpodExtensionContext): Promise { + + const packageJSON = context.extension.packageJSON; + const experiments = new ExperimentalSettings('gitpod', packageJSON.version, context.logger, context.info.getGitpodHost()); + context.subscriptions.push(experiments); + async function getPortsViewExperimentEnable(): Promise { + return (await experiments.get('gitpod.experimental.portsView.enabled', (await context.user).id, { team_ids: (await context.userTeams).map(e => e.id).join(','), }))!; + } + + const isPortsViewExperimentEnable = await getPortsViewExperimentEnable(); const portMap = new Map(); const tunnelMap = new Map(); @@ -577,6 +586,7 @@ function registerPorts(context: GitpodExtensionContext): void { } }); } + context.subscriptions.push(observePortsStatus()); context.subscriptions.push(vscode.commands.registerCommand('gitpod.resolveExternalPort', (portNumber: number) => { // eslint-disable-next-line no-async-promise-executor @@ -616,14 +626,14 @@ function registerPorts(context: GitpodExtensionContext): void { context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.makePrivate', ({ port, isWebview }: PortItem) => { context.fireAnalyticsEvent({ eventName: 'vscode_execute_command_gitpod_ports', - properties: { action: 'private', isWebview: !!isWebview } + properties: { action: 'private', isWebview: !!isWebview, userOverride: String(isUserOverrideSetting('gitpod.experimental.portsView.enabled')) } }); return port.setPortVisibility('private'); })); context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.makePublic', ({ port, isWebview }: PortItem) => { context.fireAnalyticsEvent({ eventName: 'vscode_execute_command_gitpod_ports', - properties: { action: 'public', isWebview: !!isWebview } + properties: { action: 'public', isWebview: !!isWebview, userOverride: String(isUserOverrideSetting('gitpod.experimental.portsView.enabled')) } }); return port.setPortVisibility('public'); })); @@ -636,14 +646,14 @@ function registerPorts(context: GitpodExtensionContext): void { context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.preview', ({ port, isWebview }: PortItem) => { context.fireAnalyticsEvent({ eventName: 'vscode_execute_command_gitpod_ports', - properties: { action: 'preview', isWebview: !!isWebview } + properties: { action: 'preview', isWebview: !!isWebview, userOverride: String(isUserOverrideSetting('gitpod.experimental.portsView.enabled')) } }); return openPreview(port); })); context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.openBrowser', ({ port, isWebview }: PortItem) => { context.fireAnalyticsEvent({ eventName: 'vscode_execute_command_gitpod_ports', - properties: { action: 'openBrowser', isWebview: !!isWebview } + properties: { action: 'openBrowser', isWebview: !!isWebview, userOverride: String(isUserOverrideSetting('gitpod.experimental.portsView.enabled')) } }); return openExternal(port); })); @@ -657,7 +667,7 @@ function registerPorts(context: GitpodExtensionContext): void { const portsStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); context.subscriptions.push(portsStatusBarItem); - function updateStatusBar(): void { + async function updateStatusBar(): Promise { const exposedPorts: number[] = []; for (const port of portMap.values()) { @@ -679,8 +689,8 @@ function registerPorts(context: GitpodExtensionContext): void { portsStatusBarItem.text = text; portsStatusBarItem.tooltip = tooltip; - const isPortsViewExperimentEnable = vscode.workspace.getConfiguration('gitpod.experimental.portsView').get('enabled'); - portsStatusBarItem.command = isPortsViewExperimentEnable ? 'gitpod.portsView.focus' : 'gitpod.ports.reveal'; + + portsStatusBarItem.command = (await getPortsViewExperimentEnable()) ? 'gitpod.portsView.focus' : 'gitpod.ports.reveal'; portsStatusBarItem.show(); } updateStatusBar(); @@ -820,11 +830,11 @@ function registerPorts(context: GitpodExtensionContext): void { vscode.commands.executeCommand('gitpod.api.connectLocalApp', apiPort); } })); - vscode.workspace.onDidChangeConfiguration((e: vscode.ConfigurationChangeEvent) => { + vscode.workspace.onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { if (!e.affectsConfiguration('gitpod.experimental.portsView.enabled')) { return; } - const isPortsViewExperimentEnable = vscode.workspace.getConfiguration('gitpod.experimental.portsView').get('enabled'); + const isPortsViewExperimentEnable = await getPortsViewExperimentEnable(); vscode.commands.executeCommand('setContext', 'gitpod.portsView.visible', isPortsViewExperimentEnable); gitpodWorkspaceTreeDataProvider.updateIsPortsViewExperimentEnable(isPortsViewExperimentEnable ?? false); updateStatusBar(); diff --git a/extensions/yarn.lock b/extensions/yarn.lock index b828e4933bbd7..ebbc6655445e1 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -479,6 +479,19 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +configcat-common@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/configcat-common/-/configcat-common-6.0.0.tgz#ccdb9bdafcb6a89144cac17faaab60ac960fed2a" + integrity sha512-C/lCeTKiFk9kPElRF3f4zIkvVCLKgPJuzrKbIMHCru89mvfH5t4//hZ9TW8wPJOAje6xB6ZALutDiIxggwUvWA== + +configcat-node@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/configcat-node/-/configcat-node-8.0.0.tgz#6a7d2072a848552971d91e2e44c424bfda606d21" + integrity sha512-4n4yLMpXWEiB4vmj0HuV3ArgImOEHgT+ZhP+y6N6zdwP1Z4KhQHA3btbDtZbqNw1meaVzhQMjRnpV+k/3Zr8XQ== + dependencies: + configcat-common "^6.0.0" + tunnel "0.0.6" + cookie@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" @@ -1673,6 +1686,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + typescript@4.8.2: version "4.8.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" From 9fe435289c57f42bb9d1e13c8a3c0d739e0e874f Mon Sep 17 00:00:00 2001 From: mustard Date: Fri, 9 Sep 2022 14:34:08 +0000 Subject: [PATCH 2/2] Improve vscode_execute_command_gitpod_ports event --- extensions/gitpod-shared/src/analytics.ts | 3 ++- .../portsview/src/porttable/PortLocalAddress.svelte | 8 +++++++- extensions/gitpod-web/src/extension.ts | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/extensions/gitpod-shared/src/analytics.ts b/extensions/gitpod-shared/src/analytics.ts index 2f324805a0afe..060519f2a22aa 100644 --- a/extensions/gitpod-shared/src/analytics.ts +++ b/extensions/gitpod-shared/src/analytics.ts @@ -38,8 +38,9 @@ export type GitpodAnalyticsEvent = action: 'share' | 'stop-sharing' | 'stop' | 'snapshot' | 'extend-timeout'; }> | GAET<'vscode_execute_command_gitpod_ports', { - action: 'private' | 'public' | 'preview' | 'openBrowser'; + action: 'private' | 'public' | 'preview' | 'openBrowser' | 'urlCopy'; isWebview?: boolean; + userOverride?: string; // 'true' | 'false' }> | GAET<'vscode_execute_command_gitpod_config', { action: 'remove' | 'add'; diff --git a/extensions/gitpod-web/portsview/src/porttable/PortLocalAddress.svelte b/extensions/gitpod-web/portsview/src/porttable/PortLocalAddress.svelte index 2fa2d3f7e5956..6d479f761cffd 100644 --- a/extensions/gitpod-web/portsview/src/porttable/PortLocalAddress.svelte +++ b/extensions/gitpod-web/portsview/src/porttable/PortLocalAddress.svelte @@ -36,6 +36,12 @@ function onHoverCommand(command: string) { dispatch("command", { command: command as PortCommand, port }); } + function openAddr(e: Event) { + e.preventDefault(); + if (port.status.exposed.url) { + dispatch("command", { command: "openBrowser" as PortCommand, port }); + } + } - {port.status.exposed.url} + { openAddr(e) }} href={port.status.exposed.url}>{port.status.exposed.url}