Skip to content

Commit ad6ac5f

Browse files
committed
[experiments] Add abstraction for configcat to work in self-hosted
1 parent 3e14918 commit ad6ac5f

File tree

10 files changed

+124
-114
lines changed

10 files changed

+124
-114
lines changed

components/dashboard/src/experiments/client.ts

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,11 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
import { Team } from "@gitpod/gitpod-protocol";
8-
import { newNonProductionConfigCatClient, newProductionConfigCatClient } from "./configcat";
9-
import { newAlwaysReturningDefaultValueClient } from "./always-default";
10-
11-
// Attributes define attributes which can be used to segment audiences.
12-
// Set the attributes which you want to use to group audiences into.
13-
export interface Attributes {
14-
userId?: string;
15-
email?: string;
16-
17-
// Currently selected Gitpod Project ID
18-
projectId?: string;
19-
20-
// Currently selected Gitpod Team ID
21-
teamId?: string;
22-
// Currently selected Gitpod Team Name
23-
teamName?: string;
24-
25-
// All the Gitpod Teams that the user is a member (or owner) of
26-
teams?: Array<Team>;
27-
}
28-
29-
export interface Client {
30-
getValueAsync<T>(experimentName: string, defaultValue: T, attributes: Attributes): Promise<T>;
31-
32-
// dispose will dispose of the client, no longer retrieving flags
33-
dispose(): void;
34-
}
7+
import { newAlwaysReturningDefaultValueClient } from "@gitpod/gitpod-protocol/src/experiments/always-default";
8+
import * as configcat from "configcat-js";
9+
import { ConfigCatClient } from "@gitpod/gitpod-protocol/src/experiments/configcat";
10+
import { Client } from "@gitpod/gitpod-protocol/src/experiments/types";
11+
import { LogLevel } from "configcat-common";
3512

3613
let client: Client | undefined;
3714

@@ -54,8 +31,25 @@ export function getExperimentsClient(): Client {
5431
return client;
5532
}
5633

57-
export const PROJECT_ID_ATTRIBUTE = "project_id";
58-
export const TEAM_ID_ATTRIBUTE = "team_id";
59-
export const TEAM_IDS_ATTRIBUTE = "team_ids";
60-
export const TEAM_NAME_ATTRIBUTE = "team_name";
61-
export const TEAM_NAMES_ATTRIBUTE = "team_names";
34+
// newProductionConfigCatClient constructs a new ConfigCat client with production configuration.
35+
function newProductionConfigCatClient(): Client {
36+
// clientKey is an identifier of our ConfigCat application. It is not a secret.
37+
const clientKey = "WBLaCPtkjkqKHlHedziE9g/TwAe6YyftEGPnGxVRXd0Ig";
38+
const client = configcat.createClient(clientKey, {
39+
logger: configcat.createConsoleLogger(LogLevel.Error),
40+
});
41+
42+
return new ConfigCatClient(client);
43+
}
44+
45+
// newNonProductionConfigCatClient constructs a new ConfigCat client with non-production configuration.
46+
function newNonProductionConfigCatClient(): Client {
47+
// clientKey is an identifier of our ConfigCat application. It is not a secret.
48+
const clientKey = "WBLaCPtkjkqKHlHedziE9g/LEAOCNkbuUKiqUZAcVg7dw";
49+
const client = configcat.createClient(clientKey, {
50+
pollIntervalSeconds: 60 * 3, // 3 minutes
51+
logger: configcat.createConsoleLogger(LogLevel.Info),
52+
});
53+
54+
return new ConfigCatClient(client);
55+
}

components/gitpod-protocol/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@types/react": "17.0.32",
4343
"ajv": "^6.5.4",
4444
"analytics-node": "^6.0.0",
45+
"configcat-node": "^7.0.0",
4546
"cookie": "^0.4.2",
4647
"express": "^4.17.3",
4748
"inversify": "^5.1.1",

components/dashboard/src/experiments/always-default.ts renamed to components/gitpod-protocol/src/experiments/always-default.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the GNU Affero General Public License (AGPL).
44
* See License-AGPL.txt in the project root for license information.
55
*/
6-
import { Attributes, Client } from "./client";
6+
import { Attributes, Client } from "./types";
77

88
// AlwaysReturningDefaultValueClient is an implemention of an experiments.Client which performs no lookup/network operation
99
// and always returns the default value for a given experimentName.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { Client } from "./types";
8+
import * as configcat from "configcat-node";
9+
import { LogLevel } from "configcat-common";
10+
import { ConfigCatClient } from "./configcat";
11+
import { newAlwaysReturningDefaultValueClient } from "./always-default";
12+
13+
let client: Client | undefined;
14+
15+
export function getExperimentsClientForBackend(): Client {
16+
// We have already instantiated a client, we can just re-use it.
17+
if (client !== undefined) {
18+
return client;
19+
}
20+
21+
// Retrieve SDK key from ENV Variable
22+
const sdkKey = process.env.CONFIGCAT_SDK_KEY;
23+
24+
// Self-hosted installations do not set the ConfigCat SDK key, so always use a client which returns the default value.
25+
if (sdkKey == undefined || sdkKey === "") {
26+
client = newAlwaysReturningDefaultValueClient();
27+
return client;
28+
}
29+
30+
const configCatClient = configcat.createClient(sdkKey, {
31+
logger: configcat.createConsoleLogger(LogLevel.Error),
32+
});
33+
34+
client = new ConfigCatClient(configCatClient);
35+
return client;
36+
}

components/dashboard/src/experiments/configcat.ts renamed to components/gitpod-protocol/src/experiments/configcat.ts

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,17 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
import * as configcat from "configcat-js";
8-
import { IConfigCatClient } from "configcat-common/lib/ConfigCatClient";
7+
import { Attributes, Client } from "./types";
98
import { User } from "configcat-common/lib/RolloutEvaluator";
10-
import {
11-
Attributes,
12-
Client,
13-
PROJECT_ID_ATTRIBUTE,
14-
TEAM_IDS_ATTRIBUTE,
15-
TEAM_ID_ATTRIBUTE,
16-
TEAM_NAMES_ATTRIBUTE,
17-
TEAM_NAME_ATTRIBUTE,
18-
} from "./client";
19-
20-
// newProductionConfigCatClient constructs a new ConfigCat client with production configuration.
21-
// DO NOT USE DIRECTLY! Use getExperimentsClient() instead.
22-
export function newProductionConfigCatClient(): Client {
23-
// clientKey is an identifier of our ConfigCat application. It is not a secret.
24-
const clientKey = "WBLaCPtkjkqKHlHedziE9g/TwAe6YyftEGPnGxVRXd0Ig";
25-
const client = configcat.createClient(clientKey, {
26-
logger: configcat.createConsoleLogger(2),
27-
});
28-
29-
return new ConfigCatClient(client);
30-
}
31-
32-
// newNonProductionConfigCatClient constructs a new ConfigCat client with non-production configuration.
33-
// DO NOT USE DIRECTLY! Use getExperimentsClient() instead.
34-
export function newNonProductionConfigCatClient(): Client {
35-
// clientKey is an identifier of our ConfigCat application. It is not a secret.
36-
const clientKey = "WBLaCPtkjkqKHlHedziE9g/LEAOCNkbuUKiqUZAcVg7dw";
37-
const client = configcat.createClient(clientKey, {
38-
pollIntervalSeconds: 60 * 3, // 3 minutes
39-
logger: configcat.createConsoleLogger(3),
40-
});
9+
import { IConfigCatClient } from "configcat-common/lib/ConfigCatClient";
4110

42-
return new ConfigCatClient(client);
43-
}
11+
export const PROJECT_ID_ATTRIBUTE = "project_id";
12+
export const TEAM_ID_ATTRIBUTE = "team_id";
13+
export const TEAM_IDS_ATTRIBUTE = "team_ids";
14+
export const TEAM_NAME_ATTRIBUTE = "team_name";
15+
export const TEAM_NAMES_ATTRIBUTE = "team_names";
4416

45-
class ConfigCatClient implements Client {
17+
export class ConfigCatClient implements Client {
4618
private client: IConfigCatClient;
4719

4820
constructor(cc: IConfigCatClient) {
@@ -58,7 +30,7 @@ class ConfigCatClient implements Client {
5830
}
5931
}
6032

61-
function attributesToUser(attributes: Attributes): User {
33+
export function attributesToUser(attributes: Attributes): User {
6234
const userId = attributes.userId || "";
6335
const email = attributes.email || "";
6436

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { Team } from "../teams-projects-protocol";
8+
9+
// Attributes define attributes which can be used to segment audiences.
10+
// Set the attributes which you want to use to group audiences into.
11+
export interface Attributes {
12+
userId?: string;
13+
email?: string;
14+
15+
// Currently selected Gitpod Project ID
16+
projectId?: string;
17+
18+
// Currently selected Gitpod Team ID
19+
teamId?: string;
20+
// Currently selected Gitpod Team Name
21+
teamName?: string;
22+
23+
// All the Gitpod Teams that the user is a member (or owner) of
24+
teams?: Array<Team>;
25+
}
26+
27+
export interface Client {
28+
getValueAsync<T>(experimentName: string, defaultValue: T, attributes: Attributes): Promise<T>;
29+
30+
// dispose will dispose of the client, no longer retrieving flags
31+
dispose(): void;
32+
}

components/server/ee/src/workspace/gitpod-server-impl.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ import { ClientMetadata, traceClientMetadata } from "../../../src/websocket/webs
102102
import { BitbucketAppSupport } from "../bitbucket/bitbucket-app-support";
103103
import { URL } from "url";
104104
import { UserCounter } from "../user/user-counter";
105-
import { getExperimentsClient } from "../../../src/experiments";
105+
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/src/experiments/configcat-server";
106106

107107
@injectable()
108108
export class GitpodServerEEImpl extends GitpodServerImpl {
@@ -1860,15 +1860,12 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
18601860

18611861
protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise<void> {
18621862
const teams = await this.teamDB.findTeamsByUser(user.id);
1863-
const isUsageBasedBillingEnabled = await getExperimentsClient().getValueAsync(
1863+
const isUsageBasedBillingEnabled = await getExperimentsClientForBackend().getValueAsync(
18641864
"isUsageBasedBillingEnabled",
18651865
false,
18661866
{
1867-
identifier: user.id,
1868-
custom: {
1869-
team_ids: teams.map((t) => t.id).join(","),
1870-
team_names: teams.map((t) => t.name).join(","),
1871-
},
1867+
userId: user.id,
1868+
teams: teams,
18721869
},
18731870
);
18741871
if (!isUsageBasedBillingEnabled) {

components/server/src/experiments.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
161161
import { InstallationAdminTelemetryDataProvider } from "../installation-admin/telemetry-data-provider";
162162
import { LicenseEvaluator } from "@gitpod/licensor/lib";
163163
import { Feature } from "@gitpod/licensor/lib/api";
164-
import { getExperimentsClient } from "../experiments";
165164
import { Currency } from "@gitpod/gitpod-protocol/lib/plans";
166165

167166
// shortcut
@@ -2142,19 +2141,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
21422141
await this.guardTeamOperation(params.teamId, "get");
21432142
}
21442143

2145-
const isFeatureEnabled = await getExperimentsClient().getValueAsync("isMyFirstFeatureEnabled", false, {
2146-
identifier: user.id,
2147-
custom: {
2148-
project_name: params.name,
2149-
},
2150-
});
2151-
if (isFeatureEnabled) {
2152-
throw new ResponseError(
2153-
ErrorCodes.NOT_FOUND,
2154-
`Feature is disabled for this user or project - sample usage of experiements`,
2155-
);
2156-
}
2157-
21582144
const project = this.projectsService.createProject(params, user);
21592145
this.analytics.track({
21602146
userId: user.id,

yarn.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5834,6 +5834,11 @@ configcat-common@^4.6.1, configcat-common@^4.6.2:
58345834
resolved "https://registry.yarnpkg.com/configcat-common/-/configcat-common-4.6.2.tgz#ef37114cf77c10378e686078bc45903508e0ed49"
58355835
integrity sha512-o7qp5xb3K9w3tL0dK0g2/IwzOym4SOcdl+Hgh7d1125fKamDk8Jg6nBb+jEkA0qs0msYI+kpcL7pEsihYUhEDg==
58365836

5837+
configcat-common@^6.0.0:
5838+
version "6.0.0"
5839+
resolved "https://registry.yarnpkg.com/configcat-common/-/configcat-common-6.0.0.tgz#ccdb9bdafcb6a89144cac17faaab60ac960fed2a"
5840+
integrity sha512-C/lCeTKiFk9kPElRF3f4zIkvVCLKgPJuzrKbIMHCru89mvfH5t4//hZ9TW8wPJOAje6xB6ZALutDiIxggwUvWA==
5841+
58375842
configcat-js@^5.7.2:
58385843
version "5.7.2"
58395844
resolved "https://registry.yarnpkg.com/configcat-js/-/configcat-js-5.7.2.tgz#2466269f941c8564c0d2670ffbc74dc0657f1450"
@@ -5850,6 +5855,15 @@ configcat-node@^6.7.1:
58505855
got "^9.6.0"
58515856
tunnel "0.0.6"
58525857

5858+
configcat-node@^7.0.0:
5859+
version "7.0.0"
5860+
resolved "https://registry.yarnpkg.com/configcat-node/-/configcat-node-7.0.0.tgz#50dc0f8fc866d7017e93fa3d478b7a3fe08ec815"
5861+
integrity sha512-zPU+6d/uHP4SxpBgdduI5AiPetAByGnBrF+Chpg+CuC2EmY6N4qKe5f8eWaot3NE+X4m5ylq1OMy6Li9ePIb6w==
5862+
dependencies:
5863+
configcat-common "^6.0.0"
5864+
got "^9.6.0"
5865+
tunnel "0.0.6"
5866+
58535867
configstore@^5.0.0, configstore@^5.0.1:
58545868
version "5.0.1"
58555869
resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"

0 commit comments

Comments
 (0)