Skip to content

Commit

Permalink
ConfigCat experiment
Browse files Browse the repository at this point in the history
  • Loading branch information
easyCZ authored and roboquat committed May 17, 2022
1 parent 77ce184 commit 2a852b8
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 2 deletions.
1 change: 1 addition & 0 deletions components/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"private": true,
"dependencies": {
"@gitpod/gitpod-protocol": "0.1.5",
"configcat-js": "^5.7.2",
"countries-list": "^2.6.1",
"js-cookie": "^3.0.1",
"moment": "^2.29.1",
Expand Down
24 changes: 24 additions & 0 deletions components/dashboard/src/experiments/always-default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/
import { Attributes, Client } from "./client";

// AlwaysReturningDefaultValueClient is an implemention of an experiments.Client which performs no lookup/network operation
// and always returns the default value for a given experimentName.
// This client is used for non-SaaS version of Gitpod, in particular for self-hosted installations where external
// network connections are not desirable or even possible.
class AlwaysReturningDefaultValueClient implements Client {
getValueAsync<T>(experimentName: string, defaultValue: T, attributes: Attributes): Promise<T> {
return Promise.resolve(defaultValue);
}

dispose(): void {
// there is nothing to dispose, no-op.
}
}

export function newAlwaysReturningDefaultValueClient(): Client {
return new AlwaysReturningDefaultValueClient();
}
55 changes: 55 additions & 0 deletions components/dashboard/src/experiments/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

// Attributes define attributes which can be used to segment audiences.
// Set the attributes which you want to use to group audiences into.
import { newNonProductionConfigCatClient, newProductionConfigCatClient } from "./configcat";
import { newAlwaysReturningDefaultValueClient } from "./always-default";

export interface Attributes {
userID?: string;
email?: string;

// Gitpod Project ID
projectID?: string;

// Gitpod Team ID
teamID?: string;
// Gitpod Team Name
teamName?: string;
}

export interface Client {
getValueAsync<T>(experimentName: string, defaultValue: T, attributes: Attributes): Promise<T>;

// dispose will dispose of the client, no longer retrieving flags
dispose(): void;
}

let client: Client | undefined;

export function getExperimentsClient(): Client {
// We have already instantiated a client, we can just re-use it.
if (client !== undefined) {
return client;
}

const host = window.location.hostname;
if (host === "gitpod.io") {
client = newProductionConfigCatClient();
} else if (host === "gitpod-staging.com" || host.endsWith("gitpod-dev.com") || host.endsWith("gitpod-io-dev.com")) {
client = newNonProductionConfigCatClient();
} else {
// We're gonna use a client which always returns the default value.
client = newAlwaysReturningDefaultValueClient();
}

return client;
}

export const PROJECT_ID_ATTRIBUTE = "project_id";
export const TEAM_ID_ATTRIBUTE = "team_id";
export const TEAM_NAME_ATTRIBUTE = "team_name";
69 changes: 69 additions & 0 deletions components/dashboard/src/experiments/configcat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import * as configcat from "configcat-js";
import { IConfigCatClient } from "configcat-common/lib/ConfigCatClient";
import { User } from "configcat-common/lib/RolloutEvaluator";
import { Attributes, Client, PROJECT_ID_ATTRIBUTE, TEAM_ID_ATTRIBUTE, TEAM_NAME_ATTRIBUTE } from "./client";

// newProductionConfigCatClient constructs a new ConfigCat client with production configuration.
// DO NOT USE DIRECTLY! Use getExperimentsClient() instead.
export function newProductionConfigCatClient(): Client {
// clientKey is an identifier of our ConfigCat application. It is not a secret.
const clientKey = "WBLaCPtkjkqKHlHedziE9g/TwAe6YyftEGPnGxVRXd0Ig";
const client = configcat.createClient(clientKey, {
logger: configcat.createConsoleLogger(2),
});

return new ConfigCatClient(client);
}

// newNonProductionConfigCatClient constructs a new ConfigCat client with non-production configuration.
// DO NOT USE DIRECTLY! Use getExperimentsClient() instead.
export function newNonProductionConfigCatClient(): Client {
// clientKey is an identifier of our ConfigCat application. It is not a secret.
const clientKey = "WBLaCPtkjkqKHlHedziE9g/LEAOCNkbuUKiqUZAcVg7dw";
const client = configcat.createClient(clientKey, {
pollIntervalSeconds: 60 * 3, // 3 minutes
logger: configcat.createConsoleLogger(3),
});

return new ConfigCatClient(client);
}

class ConfigCatClient implements Client {
private client: IConfigCatClient;

constructor(cc: IConfigCatClient) {
this.client = cc;
}

getValueAsync<T>(experimentName: string, defaultValue: T, attributes: Attributes): Promise<T> {
return this.client.getValueAsync(experimentName, defaultValue, attributesToUser(attributes));
}

dispose(): void {
return this.client.dispose();
}
}

function attributesToUser(attributes: Attributes): User {
const userID = attributes.userID || "";
const email = attributes.email || "";

const custom: { [key: string]: string } = {};
if (attributes.projectID) {
custom[PROJECT_ID_ATTRIBUTE] = attributes.projectID;
}
if (attributes.teamID) {
custom[TEAM_ID_ATTRIBUTE] = attributes.teamID;
}
if (attributes.teamName) {
custom[TEAM_NAME_ATTRIBUTE] = attributes.teamName;
}

return new User(userID, email, "", custom);
}
15 changes: 13 additions & 2 deletions components/dashboard/src/workspaces/Workspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { User } from "@gitpod/gitpod-protocol";
import { useLocation } from "react-router";
import { StartWorkspaceModalContext, StartWorkspaceModalKeyBinding } from "./start-workspace-modal-context";
import SelectIDEModal from "../settings/SelectIDEModal";
import { getExperimentsClient } from "../experiments/client";

export interface WorkspacesProps {}

Expand All @@ -36,6 +37,7 @@ export default function () {
const [inactiveWorkspaces, setInactiveWorkspaces] = useState<WorkspaceInfo[]>([]);
const [workspaceModel, setWorkspaceModel] = useState<WorkspaceModel>();
const { setIsStartWorkspaceModalVisible } = useContext(StartWorkspaceModalContext);
const [isExperimentEnabled, setExperiment] = useState<boolean>(false);

useEffect(() => {
(async () => {
Expand All @@ -45,13 +47,22 @@ export default function () {
}, [teams, location]);

const isOnboardingUser = user && User.isOnboardingUser(user);
useEffect(() => {
(async () => {
if (teams && teams.length > 0) {
const isEnabled = await getExperimentsClient().getValueAsync("isMyFirstFeatureEnabled", false, {
teamName: teams[0]?.name,
});
setExperiment(isEnabled);
}
})();
}, [teams]);
console.log("Is experiment enabled? ", isExperimentEnabled);

return (
<>
<Header title="Workspaces" subtitle="Manage recent and stopped workspaces." />

{isOnboardingUser && <SelectIDEModal />}

{workspaceModel?.initialized &&
(activeWorkspaces.length > 0 || inactiveWorkspaces.length > 0 || workspaceModel.searchTerm ? (
<>
Expand Down
1 change: 1 addition & 0 deletions components/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"base-64": "^1.0.0",
"bitbucket": "^2.7.0",
"body-parser": "^1.19.2",
"configcat-node": "^6.7.1",
"cookie": "^0.4.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.4",
Expand Down
22 changes: 22 additions & 0 deletions components/server/src/experiments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import * as configcat from "configcat-node";
import { IConfigCatClient } from "configcat-common/lib/ConfigCatClient";

let logger = configcat.createConsoleLogger(3); // Setting log level to 3 (Info)
let client: IConfigCatClient | undefined;

export function getExperimentsClient(): IConfigCatClient {
if (client === undefined) {
client = configcat.createClient("WBLaCPtkjkqKHlHedziE9g/LEAOCNkbuUKiqUZAcVg7dw", {
// <-- This is the actual SDK Key for your Test environment
logger: logger,
});
}

return client;
}
15 changes: 15 additions & 0 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
import { InstallationAdminTelemetryDataProvider } from "../installation-admin/telemetry-data-provider";
import { LicenseEvaluator } from "@gitpod/licensor/lib";
import { Feature } from "@gitpod/licensor/lib/api";
import { getExperimentsClient } from "../experiments";

// shortcut
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
Expand Down Expand Up @@ -2135,6 +2136,20 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
// Anyone who can read a team's information (i.e. any team member) can create a new project.
await this.guardTeamOperation(params.teamId, "get");
}

const isFeatureEnabled = await getExperimentsClient().getValueAsync("isMyFirstFeatureEnabled", false, {
identifier: user.id,
custom: {
project_name: params.name,
},
});
if (isFeatureEnabled) {
throw new ResponseError(
ErrorCodes.NOT_FOUND,
`Feature is disabled for this user or project - sample usage of experiements`,
);
}

const project = this.projectsService.createProject(params, user);
this.analytics.track({
userId: user.id,
Expand Down
26 changes: 26 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5813,6 +5813,27 @@ concurrently@^6.2.1:
tree-kill "^1.2.2"
yargs "^16.2.0"

configcat-common@^4.6.1, configcat-common@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/configcat-common/-/configcat-common-4.6.2.tgz#ef37114cf77c10378e686078bc45903508e0ed49"
integrity sha512-o7qp5xb3K9w3tL0dK0g2/IwzOym4SOcdl+Hgh7d1125fKamDk8Jg6nBb+jEkA0qs0msYI+kpcL7pEsihYUhEDg==

configcat-js@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/configcat-js/-/configcat-js-5.7.2.tgz#2466269f941c8564c0d2670ffbc74dc0657f1450"
integrity sha512-Pvryi3y1z1ZyhId5fGv6Weel6YU6EuTHHYdfY1SOaVSvNeXNU9HwLpzMUCwdINtSXyxtHd0xUMumRUje2h7/Ng==
dependencies:
configcat-common "^4.6.2"

configcat-node@^6.7.1:
version "6.7.1"
resolved "https://registry.yarnpkg.com/configcat-node/-/configcat-node-6.7.1.tgz#87c6be569d646575c969d00966a97416a4b6a4fa"
integrity sha512-+tdKZkrWo3JdRezU90daly9LmL2efCDTnjHlKMUpwtVjrNjPVXggrydrgB5QUKmJUspWUd9bFSXS3jQgoGpB4g==
dependencies:
configcat-common "^4.6.1"
got "^9.6.0"
tunnel "0.0.6"

configstore@^5.0.0, configstore@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
Expand Down Expand Up @@ -17078,6 +17099,11 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"

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

tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
Expand Down

0 comments on commit 2a852b8

Please sign in to comment.