Skip to content

Commit b5dc5f7

Browse files
mustard-mhjeanp413
authored andcommitted
Add experiments using ConfigCat with PortsView
1 parent 1493922 commit b5dc5f7

File tree

5 files changed

+137
-13
lines changed

5 files changed

+137
-13
lines changed

extensions/gitpod-shared/src/features.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require('reflect-metadata');
88
import { GitpodClient, GitpodServer, GitpodServiceImpl, WorkspaceInstanceUpdateListener } from '@gitpod/gitpod-protocol/lib/gitpod-service';
99
import { JsonRpcProxyFactory } from '@gitpod/gitpod-protocol/lib/messaging/proxy-factory';
1010
import { NavigatorContext, User } from '@gitpod/gitpod-protocol/lib/protocol';
11+
import { Team } from '@gitpod/gitpod-protocol/lib/teams-projects-protocol';
1112
import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error';
1213
import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url';
1314
import { ControlServiceClient } from '@gitpod/supervisor-api-grpc/lib/control_grpc_pb';
@@ -70,7 +71,7 @@ export class SupervisorConnection {
7071
}
7172
}
7273

73-
type UsedGitpodFunction = ['getWorkspace', 'openPort', 'stopWorkspace', 'setWorkspaceTimeout', 'getWorkspaceTimeout', 'getLoggedInUser', 'takeSnapshot', 'waitForSnapshot', 'controlAdmission', 'sendHeartBeat', 'trackEvent'];
74+
type UsedGitpodFunction = ['getWorkspace', 'openPort', 'stopWorkspace', 'setWorkspaceTimeout', 'getWorkspaceTimeout', 'getLoggedInUser', 'takeSnapshot', 'waitForSnapshot', 'controlAdmission', 'sendHeartBeat', 'trackEvent', 'getTeams'];
7475
type Union<Tuple extends any[], Union = never> = Tuple[number] | Union;
7576
export type GitpodConnection = Omit<GitpodServiceImpl<GitpodClient, GitpodServer>, 'server'> & {
7677
server: Pick<GitpodServer, Union<UsedGitpodFunction>>;
@@ -93,6 +94,7 @@ export class GitpodExtensionContext implements vscode.ExtensionContext {
9394
readonly info: WorkspaceInfoResponse,
9495
readonly owner: Promise<User>,
9596
readonly user: Promise<User>,
97+
readonly userTeams: Promise<Team[]>,
9698
readonly instanceListener: Promise<WorkspaceInstanceUpdateListener>,
9799
readonly workspaceOwned: Promise<boolean>,
98100
readonly logger: Log,
@@ -241,7 +243,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont
241243
const gitpodApi = workspaceInfo.getGitpodApi()!;
242244

243245
const factory = new JsonRpcProxyFactory<GitpodServer>();
244-
const gitpodFunctions: UsedGitpodFunction = ['getWorkspace', 'openPort', 'stopWorkspace', 'setWorkspaceTimeout', 'getWorkspaceTimeout', 'getLoggedInUser', 'takeSnapshot', 'waitForSnapshot', 'controlAdmission', 'sendHeartBeat', 'trackEvent'];
246+
const gitpodFunctions: UsedGitpodFunction = ['getWorkspace', 'openPort', 'stopWorkspace', 'setWorkspaceTimeout', 'getWorkspaceTimeout', 'getLoggedInUser', 'takeSnapshot', 'waitForSnapshot', 'controlAdmission', 'sendHeartBeat', 'trackEvent', 'getTeams'];
245247
const gitpodService: GitpodConnection = new GitpodServiceImpl<GitpodClient, GitpodServer>(factory.createProxy()) as any;
246248
const gitpodScopes = new Set<string>([
247249
'resource:workspace::' + workspaceId + '::get/update',
@@ -302,6 +304,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont
302304
}
303305
return vscode.commands.executeCommand('gitpod.api.getLoggedInUser') as typeof pendingGetOwner;
304306
})();
307+
const pendingGetUserTeams = gitpodService.server.getTeams();
305308
const pendingInstanceListener = gitpodService.listenToInstance(workspaceId);
306309
const pendingWorkspaceOwned = (async () => {
307310
const owner = await pendingGetOwner;
@@ -325,6 +328,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont
325328
workspaceInfo,
326329
pendingGetOwner,
327330
pendingGetUser,
331+
pendingGetUserTeams,
328332
pendingInstanceListener,
329333
pendingWorkspaceOwned,
330334
logger,

extensions/gitpod-web/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@
562562
"dependencies": {
563563
"@gitpod/gitpod-protocol": "main",
564564
"@gitpod/supervisor-api-grpc": "main",
565+
"configcat-node": "^8.0.0",
565566
"gitpod-shared": "0.0.1",
566567
"node-fetch": "2.6.7",
567568
"uuid": "8.1.0",
+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
import * as vscode from 'vscode';
6+
import * as configcat from 'configcat-node';
7+
import * as configcatcommon from 'configcat-common';
8+
import * as semver from 'semver';
9+
import Log from 'gitpod-shared/out/common/logger';
10+
import { URL } from 'url';
11+
12+
const EXPERTIMENTAL_SETTINGS = [
13+
'gitpod.experimental.portsView.enabled',
14+
];
15+
16+
export class ExperimentalSettings {
17+
private configcatClient: configcatcommon.IConfigCatClient;
18+
private extensionVersion: semver.SemVer;
19+
20+
constructor(key: string, extensionVersion: string, private logger: Log, gitpodHost: string) {
21+
this.configcatClient = configcat.createClientWithLazyLoad(key, {
22+
baseUrl: new URL('/configcat', process.env['VSCODE_DEV'] ? 'https://gitpod-staging.com' : gitpodHost).href,
23+
logger: {
24+
debug(): void { },
25+
log(): void { },
26+
info(): void { },
27+
warn(message: string): void { logger.warn(`ConfigCat: ${message}`); },
28+
error(message: string): void { logger.error(`ConfigCat: ${message}`); }
29+
},
30+
requestTimeoutMs: 1500,
31+
cacheTimeToLiveSeconds: 60
32+
});
33+
this.extensionVersion = new semver.SemVer(extensionVersion);
34+
}
35+
36+
async get<T>(key: string, userId?: string, custom?: { [key: string]: string }): Promise<T | undefined> {
37+
const config = vscode.workspace.getConfiguration('gitpod');
38+
const values = config.inspect<T>(key.substring('gitpod.'.length));
39+
if (!values || !EXPERTIMENTAL_SETTINGS.includes(key)) {
40+
this.logger.error(`Cannot get invalid experimental setting '${key}'`);
41+
return values?.globalValue ?? values?.defaultValue;
42+
}
43+
if (this.isPreRelease()) {
44+
// PreRelease versions always have experiments enabled by default
45+
return values.globalValue ?? values.defaultValue;
46+
}
47+
if (values.globalValue !== undefined) {
48+
// User setting have priority over configcat so return early
49+
return values.globalValue;
50+
}
51+
52+
const user = userId ? new configcatcommon.User(userId, undefined, undefined, custom) : undefined;
53+
const configcatKey = key.replace(/\./g, '_'); // '.' are not allowed in configcat
54+
const experimentValue = (await this.configcatClient.getValueAsync(configcatKey, undefined, user)) as T | undefined;
55+
56+
return experimentValue ?? values.defaultValue;
57+
}
58+
59+
async inspect<T>(key: string, userId?: string, custom?: { [key: string]: string }): Promise<{ key: string; defaultValue?: T; globalValue?: T; experimentValue?: T } | undefined> {
60+
const config = vscode.workspace.getConfiguration('gitpod');
61+
const values = config.inspect<T>(key.substring('gitpod.'.length));
62+
if (!values || !EXPERTIMENTAL_SETTINGS.includes(key)) {
63+
this.logger.error(`Cannot inspect invalid experimental setting '${key}'`);
64+
return values;
65+
}
66+
67+
const user = userId ? new configcatcommon.User(userId, undefined, undefined, custom) : undefined;
68+
const configcatKey = key.replace(/\./g, '_'); // '.' are not allowed in configcat
69+
const experimentValue = (await this.configcatClient.getValueAsync(configcatKey, undefined, user)) as T | undefined;
70+
71+
return { key, defaultValue: values.defaultValue, globalValue: values.globalValue, experimentValue };
72+
}
73+
74+
forceRefreshAsync(): Promise<void> {
75+
return this.configcatClient.forceRefreshAsync();
76+
}
77+
78+
private isPreRelease() {
79+
return this.extensionVersion.minor % 2 === 1;
80+
}
81+
82+
dispose(): void {
83+
this.configcatClient.dispose();
84+
}
85+
}
86+
87+
export function isUserOverrideSetting(key: string): boolean {
88+
const config = vscode.workspace.getConfiguration('gitpod');
89+
const values = config.inspect(key.substring('gitpod.'.length));
90+
return values?.globalValue !== undefined;
91+
}

extensions/gitpod-web/src/extension.ts

+21-11
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { getManifest } from './util/extensionManagmentUtill';
2525
import { GitpodWorkspacePort, PortInfo, iconStatusMap } from './util/port';
2626
import { ReleaseNotes } from './releaseNotes';
2727
import { registerWelcomeWalkthroughContribution, WELCOME_WALKTROUGH_KEY } from './welcomeWalktrough';
28+
import { ExperimentalSettings, isUserOverrideSetting } from './experiments';
2829

2930
let gitpodContext: GitpodExtensionContext | undefined;
3031
export async function activate(context: vscode.ExtensionContext) {
@@ -516,8 +517,16 @@ function getNonce() {
516517

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

519-
function registerPorts(context: GitpodExtensionContext): void {
520-
const isPortsViewExperimentEnable = vscode.workspace.getConfiguration('gitpod.experimental.portsView').get<boolean>('enabled');
520+
async function registerPorts(context: GitpodExtensionContext): Promise<void> {
521+
522+
const packageJSON = context.extension.packageJSON;
523+
const experiments = new ExperimentalSettings('gitpod', packageJSON.version, context.logger, context.info.getGitpodHost());
524+
context.subscriptions.push(experiments);
525+
async function getPortsViewExperimentEnable(): Promise<boolean> {
526+
return (await experiments.get<boolean>('gitpod.experimental.portsView.enabled', (await context.user).id, { team_ids: (await context.userTeams).map(e => e.id).join(','), }))!;
527+
}
528+
529+
const isPortsViewExperimentEnable = await getPortsViewExperimentEnable();
521530

522531
const portMap = new Map<number, GitpodWorkspacePort>();
523532
const tunnelMap = new Map<number, vscode.TunnelDescription>();
@@ -577,6 +586,7 @@ function registerPorts(context: GitpodExtensionContext): void {
577586
}
578587
});
579588
}
589+
580590
context.subscriptions.push(observePortsStatus());
581591
context.subscriptions.push(vscode.commands.registerCommand('gitpod.resolveExternalPort', (portNumber: number) => {
582592
// eslint-disable-next-line no-async-promise-executor
@@ -616,14 +626,14 @@ function registerPorts(context: GitpodExtensionContext): void {
616626
context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.makePrivate', ({ port, isWebview }: PortItem) => {
617627
context.fireAnalyticsEvent({
618628
eventName: 'vscode_execute_command_gitpod_ports',
619-
properties: { action: 'private', isWebview: !!isWebview }
629+
properties: { action: 'private', isWebview: !!isWebview, userOverride: String(isUserOverrideSetting('gitpod.experimental.portsView.enabled')) }
620630
});
621631
return port.setPortVisibility('private');
622632
}));
623633
context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.makePublic', ({ port, isWebview }: PortItem) => {
624634
context.fireAnalyticsEvent({
625635
eventName: 'vscode_execute_command_gitpod_ports',
626-
properties: { action: 'public', isWebview: !!isWebview }
636+
properties: { action: 'public', isWebview: !!isWebview, userOverride: String(isUserOverrideSetting('gitpod.experimental.portsView.enabled')) }
627637
});
628638
return port.setPortVisibility('public');
629639
}));
@@ -636,14 +646,14 @@ function registerPorts(context: GitpodExtensionContext): void {
636646
context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.preview', ({ port, isWebview }: PortItem) => {
637647
context.fireAnalyticsEvent({
638648
eventName: 'vscode_execute_command_gitpod_ports',
639-
properties: { action: 'preview', isWebview: !!isWebview }
649+
properties: { action: 'preview', isWebview: !!isWebview, userOverride: String(isUserOverrideSetting('gitpod.experimental.portsView.enabled')) }
640650
});
641651
return openPreview(port);
642652
}));
643653
context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.openBrowser', ({ port, isWebview }: PortItem) => {
644654
context.fireAnalyticsEvent({
645655
eventName: 'vscode_execute_command_gitpod_ports',
646-
properties: { action: 'openBrowser', isWebview: !!isWebview }
656+
properties: { action: 'openBrowser', isWebview: !!isWebview, userOverride: String(isUserOverrideSetting('gitpod.experimental.portsView.enabled')) }
647657
});
648658
return openExternal(port);
649659
}));
@@ -657,7 +667,7 @@ function registerPorts(context: GitpodExtensionContext): void {
657667

658668
const portsStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
659669
context.subscriptions.push(portsStatusBarItem);
660-
function updateStatusBar(): void {
670+
async function updateStatusBar(): Promise<void> {
661671
const exposedPorts: number[] = [];
662672

663673
for (const port of portMap.values()) {
@@ -679,8 +689,8 @@ function registerPorts(context: GitpodExtensionContext): void {
679689

680690
portsStatusBarItem.text = text;
681691
portsStatusBarItem.tooltip = tooltip;
682-
const isPortsViewExperimentEnable = vscode.workspace.getConfiguration('gitpod.experimental.portsView').get<boolean>('enabled');
683-
portsStatusBarItem.command = isPortsViewExperimentEnable ? 'gitpod.portsView.focus' : 'gitpod.ports.reveal';
692+
693+
portsStatusBarItem.command = (await getPortsViewExperimentEnable()) ? 'gitpod.portsView.focus' : 'gitpod.ports.reveal';
684694
portsStatusBarItem.show();
685695
}
686696
updateStatusBar();
@@ -820,11 +830,11 @@ function registerPorts(context: GitpodExtensionContext): void {
820830
vscode.commands.executeCommand('gitpod.api.connectLocalApp', apiPort);
821831
}
822832
}));
823-
vscode.workspace.onDidChangeConfiguration((e: vscode.ConfigurationChangeEvent) => {
833+
vscode.workspace.onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
824834
if (!e.affectsConfiguration('gitpod.experimental.portsView.enabled')) {
825835
return;
826836
}
827-
const isPortsViewExperimentEnable = vscode.workspace.getConfiguration('gitpod.experimental.portsView').get<boolean>('enabled');
837+
const isPortsViewExperimentEnable = await getPortsViewExperimentEnable();
828838
vscode.commands.executeCommand('setContext', 'gitpod.portsView.visible', isPortsViewExperimentEnable);
829839
gitpodWorkspaceTreeDataProvider.updateIsPortsViewExperimentEnable(isPortsViewExperimentEnable ?? false);
830840
updateStatusBar();

extensions/yarn.lock

+18
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,19 @@ [email protected]:
479479
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
480480
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
481481

482+
configcat-common@^6.0.0:
483+
version "6.0.0"
484+
resolved "https://registry.yarnpkg.com/configcat-common/-/configcat-common-6.0.0.tgz#ccdb9bdafcb6a89144cac17faaab60ac960fed2a"
485+
integrity sha512-C/lCeTKiFk9kPElRF3f4zIkvVCLKgPJuzrKbIMHCru89mvfH5t4//hZ9TW8wPJOAje6xB6ZALutDiIxggwUvWA==
486+
487+
configcat-node@^8.0.0:
488+
version "8.0.0"
489+
resolved "https://registry.yarnpkg.com/configcat-node/-/configcat-node-8.0.0.tgz#6a7d2072a848552971d91e2e44c424bfda606d21"
490+
integrity sha512-4n4yLMpXWEiB4vmj0HuV3ArgImOEHgT+ZhP+y6N6zdwP1Z4KhQHA3btbDtZbqNw1meaVzhQMjRnpV+k/3Zr8XQ==
491+
dependencies:
492+
configcat-common "^6.0.0"
493+
tunnel "0.0.6"
494+
482495
cookie@^0.4.2:
483496
version "0.4.2"
484497
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
@@ -1673,6 +1686,11 @@ tr46@~0.0.3:
16731686
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
16741687
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
16751688

1689+
1690+
version "0.0.6"
1691+
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
1692+
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
1693+
16761694
16771695
version "4.8.2"
16781696
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790"

0 commit comments

Comments
 (0)