Skip to content

Commit 8b096e3

Browse files
authored
Add experiments using ConfigCat (#15)
1 parent 2dc10bf commit 8b096e3

File tree

8 files changed

+171
-192
lines changed

8 files changed

+171
-192
lines changed

.github/workflows/nightly.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ jobs:
2323
run: |
2424
set -e
2525
setSegmentKey="setpath([\"segmentKey\"]; \"${{ secrets.ANALITYCS_KEY }}\")"
26-
jqCommands="${setSegmentKey}"
26+
setConfigcatKey="setpath([\"configcatKey\"]; \"${{ secrets.CONFIGCAT_KEY }}\")"
27+
jqCommands="${setSegmentKey} | ${setConfigcatKey}"
2728
cat package.json | jq "${jqCommands}" > package.json.tmp
2829
mv package.json.tmp package.json
2930

.github/workflows/release.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ jobs:
2020
run: |
2121
set -e
2222
setSegmentKey="setpath([\"segmentKey\"]; \"${{ secrets.ANALITYCS_KEY }}\")"
23-
jqCommands="${setSegmentKey}"
23+
setConfigcatKey="setpath([\"configcatKey\"]; \"${{ secrets.CONFIGCAT_KEY }}\")"
24+
jqCommands="${setSegmentKey} | ${setConfigcatKey}"
2425
cat package.json | jq "${jqCommands}" > package.json.tmp
2526
mv package.json.tmp package.json
2627

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
},
105105
"main": "./out/extension.js",
106106
"segmentKey": "YErmvd89wPsrCuGcVnF2XAl846W9WIGl",
107+
"configcatKey": "WBLaCPtkjkqKHlHedziE9g/LEAOCNkbuUKiqUZAcVg7dw",
107108
"scripts": {
108109
"vscode:prepublish": "webpack --mode production",
109110
"webpack": "webpack --mode development",
@@ -145,7 +146,8 @@
145146
"@gitpod/gitpod-protocol": "main",
146147
"@gitpod/local-app-api-grpcweb": "main",
147148
"@improbable-eng/grpc-web-node-http-transport": "^0.14.0",
148-
"analytics-node": "^6.0.0",
149+
"analytics-node": "^6.2.0",
150+
"configcat-node": "^8.0.0",
149151
"js-yaml": "^4.1.0",
150152
"node-fetch": "2.6.7",
151153
"pkce-challenge": "^3.0.0",

src/common/logger.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ export default class Log {
4040
this.logLevel('Warn', message, data);
4141
}
4242

43+
/**
44+
* @deprecated Use `Log.trace` instead
45+
*/
4346
public log(message: string, data?: any): void {
44-
this.logLevel('Log', message, data);
47+
this.trace(message, data);
4548
}
4649

4750
public logLevel(level: LogLevel, message: string, data?: any): void {

src/experiments.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import * as configcat from 'configcat-node';
8+
import * as configcatcommon from 'configcat-common';
9+
import * as semver from 'semver';
10+
import Log from './common/logger';
11+
12+
const EXPERTIMENTAL_SETTINGS = [
13+
'gitpod.remote.useLocalApp'
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) {
21+
this.configcatClient = configcat.createClientWithLazyLoad(key, {
22+
logger: {
23+
debug(): void { },
24+
log(): void { },
25+
info(): void { },
26+
warn(message: string): void { logger.warn(`ConfigCat: ${message}`); },
27+
error(message: string): void { logger.error(`ConfigCat: ${message}`); }
28+
},
29+
requestTimeoutMs: 1500,
30+
cacheTimeToLiveSeconds: 60
31+
});
32+
this.extensionVersion = new semver.SemVer(extensionVersion);
33+
}
34+
35+
async get<T>(key: string, userId?: string): Promise<T | undefined> {
36+
const config = vscode.workspace.getConfiguration('gitpod');
37+
const values = config.inspect<T>(key.substring('gitpod.'.length));
38+
if (!values || !EXPERTIMENTAL_SETTINGS.includes(key)) {
39+
this.logger.error(`Cannot get invalid experimental setting '${key}'`);
40+
return values?.globalValue ?? values?.defaultValue;
41+
}
42+
if (this.isPreRelease()) {
43+
// PreRelease versions always have experiments enabled by default
44+
return values.globalValue ?? values.defaultValue;
45+
}
46+
if (values.globalValue !== undefined) {
47+
// User setting have priority over configcat so return early
48+
return values.globalValue;
49+
}
50+
51+
const user = userId ? new configcatcommon.User(userId) : undefined;
52+
const configcatKey = key.replace(/\./g, '_'); // '.' are not allowed in configcat
53+
const experimentValue = (await this.configcatClient.getValueAsync(configcatKey, undefined, user)) as T | undefined;
54+
55+
return experimentValue ?? values.defaultValue;
56+
}
57+
58+
async inspect<T>(key: string, userId?: string): Promise<{ key: string; defaultValue?: T; globalValue?: T; experimentValue?: T } | undefined> {
59+
const config = vscode.workspace.getConfiguration('gitpod');
60+
const values = config.inspect<T>(key.substring('gitpod.'.length));
61+
if (!values || !EXPERTIMENTAL_SETTINGS.includes(key)) {
62+
this.logger.error(`Cannot inspect invalid experimental setting '${key}'`);
63+
return values;
64+
}
65+
66+
const user = userId ? new configcatcommon.User(userId) : undefined;
67+
const configcatKey = key.replace(/\./g, '_'); // '.' are not allowed in configcat
68+
const experimentValue = (await this.configcatClient.getValueAsync(configcatKey, undefined, user)) as T | undefined;
69+
70+
return { key, defaultValue: values.defaultValue, globalValue: values.globalValue, experimentValue };
71+
}
72+
73+
isUserOverride(key: string): boolean {
74+
const config = vscode.workspace.getConfiguration('gitpod');
75+
const values = config.inspect(key.substring('gitpod.'.length));
76+
return values?.globalValue !== undefined;
77+
}
78+
79+
forceRefreshAsync(): Promise<void> {
80+
return this.configcatClient.forceRefreshAsync();
81+
}
82+
83+
private isPreRelease() {
84+
return this.extensionVersion.minor % 2 === 1;
85+
}
86+
87+
dispose(): void {
88+
this.configcatClient.dispose();
89+
}
90+
}

src/extension.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import GitpodServer from './gitpodServer';
1313
import TelemetryReporter from './telemetryReporter';
1414
import { exportLogs } from './exportLogs';
1515
import { registerReleaseNotesView } from './releaseNotes';
16+
import { ExperimentalSettings } from './experiments';
1617

1718
const FIRST_INSTALL_KEY = 'gitpod-desktop.firstInstall';
1819

@@ -26,6 +27,9 @@ export async function activate(context: vscode.ExtensionContext) {
2627
const logger = new Log('Gitpod');
2728
logger.info(`${extensionId}/${packageJSON.version} (${os.release()} ${os.platform()} ${os.arch()}) vscode/${vscode.version} (${vscode.env.appName})`);
2829

30+
const experiments = new ExperimentalSettings(packageJSON.configcatKey, packageJSON.version, logger);
31+
context.subscriptions.push(experiments);
32+
2933
telemetry = new TelemetryReporter(extensionId, packageJSON.version, packageJSON.segmentKey);
3034

3135
context.subscriptions.push(vscode.commands.registerCommand('gitpod.exportLogs', async () => {
@@ -41,7 +45,7 @@ export async function activate(context: vscode.ExtensionContext) {
4145
context.subscriptions.push(new SettingsSync(logger, telemetry));
4246

4347
const authProvider = new GitpodAuthenticationProvider(context, logger, telemetry);
44-
remoteConnector = new RemoteConnector(context, logger, telemetry);
48+
remoteConnector = new RemoteConnector(context, experiments, logger, telemetry);
4549
context.subscriptions.push(authProvider);
4650
context.subscriptions.push(vscode.window.registerUriHandler({
4751
handleUri(uri: vscode.Uri) {

src/remoteConnector.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { getGitpodVersion, isFeatureSupported } from './featureSupport';
2929
import SSHConfiguration from './ssh/sshConfig';
3030
import { isWindows } from './common/platform';
3131
import { untildify } from './common/files';
32+
import { ExperimentalSettings } from './experiments';
3233

3334
interface SSHConnectionParams {
3435
workspaceId: string;
@@ -118,7 +119,12 @@ export default class RemoteConnector extends Disposable {
118119

119120
private heartbeatManager: HeartbeatManager | undefined;
120121

121-
constructor(private readonly context: vscode.ExtensionContext, private readonly logger: Log, private readonly telemetry: TelemetryReporter) {
122+
constructor(
123+
private readonly context: vscode.ExtensionContext,
124+
private readonly experiments: ExperimentalSettings,
125+
private readonly logger: Log,
126+
private readonly telemetry: TelemetryReporter
127+
) {
122128
super();
123129

124130
if (isGitpodRemoteWindow(context)) {
@@ -770,11 +776,13 @@ export default class RemoteConnector extends Disposable {
770776

771777
this.logger.info('Opening Gitpod workspace', uri.toString());
772778

779+
const userOverride = this.experiments.isUserOverride('gitpod.remote.useLocalApp');
773780
const forceUseLocalApp = vscode.workspace.getConfiguration('gitpod').get<boolean>('remote.useLocalApp')!;
781+
// const forceUseLocalApp = (await this.experiments.get<boolean>('gitpod.remote.useLocalApp', session.account.id))!;
774782
let sshDestination: string | undefined;
775783
if (!forceUseLocalApp) {
776784
try {
777-
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'gateway', status: 'connecting', ...params, gitpodVersion: gitpodVersion.raw });
785+
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'gateway', status: 'connecting', ...params, gitpodVersion: gitpodVersion.raw, userOverride });
778786

779787
const { destination, password } = await this.getWorkspaceSSHDestination(session.accessToken, params);
780788
sshDestination = destination;
@@ -783,9 +791,9 @@ export default class RemoteConnector extends Disposable {
783791
await this.showSSHPasswordModal(password, params);
784792
}
785793

786-
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'gateway', status: 'connected', ...params, gitpodVersion: gitpodVersion.raw });
794+
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'gateway', status: 'connected', ...params, gitpodVersion: gitpodVersion.raw, userOverride });
787795
} catch (e) {
788-
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'gateway', status: 'failed', reason: e.toString(), ...params, gitpodVersion: gitpodVersion.raw });
796+
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'gateway', status: 'failed', reason: e.toString(), ...params, gitpodVersion: gitpodVersion.raw, userOverride });
789797
if (e instanceof NoSSHGatewayError) {
790798
this.logger.error('No SSH gateway:', e);
791799
vscode.window.showWarningMessage(`${e.host} does not support [direct SSH access](https://github.com/gitpod-io/gitpod/blob/main/install/installer/docs/workspace-ssh-access.md), connecting via the deprecated SSH tunnel over WebSocket.`);
@@ -817,15 +825,15 @@ export default class RemoteConnector extends Disposable {
817825
let localAppSSHConfigPath: string | undefined;
818826
if (!usingSSHGateway) {
819827
try {
820-
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'local-app', status: 'connecting', ...params, gitpodVersion: gitpodVersion.raw });
828+
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'local-app', status: 'connecting', ...params, gitpodVersion: gitpodVersion.raw, userOverride });
821829

822830
const localAppDestData = await this.getWorkspaceLocalAppSSHDestination(params);
823831
sshDestination = localAppDestData.localAppSSHDest;
824832
localAppSSHConfigPath = localAppDestData.localAppSSHConfigPath;
825833

826-
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'local-app', status: 'connected', ...params, gitpodVersion: gitpodVersion.raw });
834+
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'local-app', status: 'connected', ...params, gitpodVersion: gitpodVersion.raw, userOverride });
827835
} catch (e) {
828-
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'local-app', status: 'failed', reason: e.toString(), ...params, gitpodVersion: gitpodVersion.raw });
836+
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'local-app', status: 'failed', reason: e.toString(), ...params, gitpodVersion: gitpodVersion.raw, userOverride });
829837
this.logger.error(`Failed to connect ${params.workspaceId} Gitpod workspace:`, e);
830838
if (e instanceof LocalAppError) {
831839
const seeLogs = 'See Logs';

0 commit comments

Comments
 (0)