Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experiments using ConfigCat with PortsView #427

Merged
merged 2 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion extensions/gitpod-shared/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
8 changes: 6 additions & 2 deletions extensions/gitpod-shared/src/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 extends any[], Union = never> = Tuple[number] | Union;
export type GitpodConnection = Omit<GitpodServiceImpl<GitpodClient, GitpodServer>, 'server'> & {
server: Pick<GitpodServer, Union<UsedGitpodFunction>>;
Expand All @@ -93,6 +94,7 @@ export class GitpodExtensionContext implements vscode.ExtensionContext {
readonly info: WorkspaceInfoResponse,
readonly owner: Promise<User>,
readonly user: Promise<User>,
readonly userTeams: Promise<Team[]>,
readonly instanceListener: Promise<WorkspaceInstanceUpdateListener>,
readonly workspaceOwned: Promise<boolean>,
readonly logger: Log,
Expand Down Expand Up @@ -241,7 +243,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont
const gitpodApi = workspaceInfo.getGitpodApi()!;

const factory = new JsonRpcProxyFactory<GitpodServer>();
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<GitpodClient, GitpodServer>(factory.createProxy()) as any;
const gitpodScopes = new Set<string>([
'resource:workspace::' + workspaceId + '::get/update',
Expand Down Expand Up @@ -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;
Expand All @@ -325,6 +328,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont
workspaceInfo,
pendingGetOwner,
pendingGetUser,
pendingGetUserTeams,
pendingInstanceListener,
pendingWorkspaceOwned,
logger,
Expand Down
1 change: 1 addition & 0 deletions extensions/gitpod-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
</script>

<HoverOptions
Expand All @@ -45,7 +51,7 @@
onHoverCommand(e.detail);
}}
>
<a href={port.status.exposed.url}>{port.status.exposed.url}</a>
<a on:click={(e) => { openAddr(e) }} href={port.status.exposed.url}>{port.status.exposed.url}</a>
</HoverOptions>

<style>
Expand Down
91 changes: 91 additions & 0 deletions extensions/gitpod-web/src/experiments.ts
Original file line number Diff line number Diff line change
@@ -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<T>(key: string, userId?: string, custom?: { [key: string]: string }): Promise<T | undefined> {
const config = vscode.workspace.getConfiguration('gitpod');
const values = config.inspect<T>(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<T>(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<T>(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<void> {
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;
}
36 changes: 25 additions & 11 deletions extensions/gitpod-web/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -497,6 +498,10 @@ class GitpodPortViewProvider implements vscode.WebviewViewProvider {
if (!port) { return; }
if (message.command === 'urlCopy' && port.status.exposed) {
await vscode.env.clipboard.writeText(port.status.exposed.url);
this.context.fireAnalyticsEvent({
eventName: 'vscode_execute_command_gitpod_ports',
properties: { action: 'urlCopy', isWebview: true, userOverride: String(isUserOverrideSetting('gitpod.experimental.portsView.enabled')) }
});
return;
}
vscode.commands.executeCommand('gitpod.ports.' + message.command, { port, isWebview: true });
Expand All @@ -516,8 +521,16 @@ function getNonce() {

interface PortItem { port: GitpodWorkspacePort; isWebview?: boolean }

function registerPorts(context: GitpodExtensionContext): void {
const isPortsViewExperimentEnable = vscode.workspace.getConfiguration('gitpod.experimental.portsView').get<boolean>('enabled');
async function registerPorts(context: GitpodExtensionContext): Promise<void> {

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<boolean> {
return (await experiments.get<boolean>('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<number, GitpodWorkspacePort>();
const tunnelMap = new Map<number, vscode.TunnelDescription>();
Expand Down Expand Up @@ -577,6 +590,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
Expand Down Expand Up @@ -616,14 +630,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');
}));
Expand All @@ -636,14 +650,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);
}));
Expand All @@ -657,7 +671,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<void> {
const exposedPorts: number[] = [];

for (const port of portMap.values()) {
Expand All @@ -679,8 +693,8 @@ function registerPorts(context: GitpodExtensionContext): void {

portsStatusBarItem.text = text;
portsStatusBarItem.tooltip = tooltip;
const isPortsViewExperimentEnable = vscode.workspace.getConfiguration('gitpod.experimental.portsView').get<boolean>('enabled');
portsStatusBarItem.command = isPortsViewExperimentEnable ? 'gitpod.portsView.focus' : 'gitpod.ports.reveal';

portsStatusBarItem.command = (await getPortsViewExperimentEnable()) ? 'gitpod.portsView.focus' : 'gitpod.ports.reveal';
portsStatusBarItem.show();
}
updateStatusBar();
Expand Down Expand Up @@ -820,11 +834,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<boolean>('enabled');
const isPortsViewExperimentEnable = await getPortsViewExperimentEnable();
vscode.commands.executeCommand('setContext', 'gitpod.portsView.visible', isPortsViewExperimentEnable);
gitpodWorkspaceTreeDataProvider.updateIsPortsViewExperimentEnable(isPortsViewExperimentEnable ?? false);
updateStatusBar();
Expand Down
18 changes: 18 additions & 0 deletions extensions/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,19 @@ [email protected]:
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"
Expand Down Expand Up @@ -1673,6 +1686,11 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=

[email protected]:
version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==

[email protected]:
version "4.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790"
Expand Down