Skip to content

[experiments] Add abstraction for configcat to work in self-hosted #10807

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

Merged
merged 1 commit into from
Jun 27, 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
62 changes: 29 additions & 33 deletions components/dashboard/src/experiments/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,11 @@
* See License-AGPL.txt in the project root for license information.
*/

import { Team } from "@gitpod/gitpod-protocol";
import { newNonProductionConfigCatClient, newProductionConfigCatClient } from "./configcat";
import { newAlwaysReturningDefaultValueClient } from "./always-default";

// Attributes define attributes which can be used to segment audiences.
// Set the attributes which you want to use to group audiences into.
export interface Attributes {
userId?: string;
email?: string;

// Currently selected Gitpod Project ID
projectId?: string;

// Currently selected Gitpod Team ID
teamId?: string;
// Currently selected Gitpod Team Name
teamName?: string;

// All the Gitpod Teams that the user is a member (or owner) of
teams?: Array<Team>;
}

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;
}
import { newAlwaysReturningDefaultValueClient } from "@gitpod/gitpod-protocol/lib/experiments/always-default";
import * as configcat from "configcat-js";
import { ConfigCatClient } from "@gitpod/gitpod-protocol/lib/experiments/configcat";
import { Client } from "@gitpod/gitpod-protocol/lib/experiments/types";
import { LogLevel } from "configcat-common";

let client: Client | undefined;

Expand All @@ -54,8 +31,27 @@ export function getExperimentsClient(): Client {
return client;
}

export const PROJECT_ID_ATTRIBUTE = "project_id";
export const TEAM_ID_ATTRIBUTE = "team_id";
export const TEAM_IDS_ATTRIBUTE = "team_ids";
export const TEAM_NAME_ATTRIBUTE = "team_name";
export const TEAM_NAMES_ATTRIBUTE = "team_names";
// newProductionConfigCatClient constructs a new ConfigCat client with production configuration.
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(LogLevel.Error),
maxInitWaitTimeSeconds: 0,
});

return new ConfigCatClient(client);
}

// newNonProductionConfigCatClient constructs a new ConfigCat client with non-production configuration.
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(LogLevel.Info),
maxInitWaitTimeSeconds: 0,
});

return new ConfigCatClient(client);
}
83 changes: 0 additions & 83 deletions components/dashboard/src/experiments/configcat.ts

This file was deleted.

1 change: 1 addition & 0 deletions components/gitpod-protocol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@types/react": "17.0.32",
"ajv": "^6.5.4",
"analytics-node": "^6.0.0",
"configcat-node": "^7.0.0",
"cookie": "^0.4.2",
"express": "^4.17.3",
"inversify": "^5.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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";
import { Attributes, Client } from "./types";

// AlwaysReturningDefaultValueClient is an implemention of an experiments.Client which performs no lookup/network operation
// and always returns the default value for a given experimentName.
Expand Down
37 changes: 37 additions & 0 deletions components/gitpod-protocol/src/experiments/configcat-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 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 { Client } from "./types";
import * as configcat from "configcat-node";
import { LogLevel } from "configcat-common";
import { ConfigCatClient } from "./configcat";
import { newAlwaysReturningDefaultValueClient } from "./always-default";

let client: Client | undefined;

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

// Retrieve SDK key from ENV Variable
const sdkKey = process.env.CONFIGCAT_SDK_KEY;

// Self-hosted installations do not set the ConfigCat SDK key, so always use a client which returns the default value.
if (sdkKey === undefined || sdkKey === "") {
client = newAlwaysReturningDefaultValueClient();
return client;
}

const configCatClient = configcat.createClient(sdkKey, {
logger: configcat.createConsoleLogger(LogLevel.Error),
maxInitWaitTimeSeconds: 0,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jankeromnes I've pulled in your change into my refactor as well

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Is this ready for review?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is :)

});

client = new ConfigCatClient(configCatClient);
return client;
}
53 changes: 53 additions & 0 deletions components/gitpod-protocol/src/experiments/configcat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* 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 "./types";
import { User } from "configcat-common/lib/RolloutEvaluator";
import { IConfigCatClient } from "configcat-common/lib/ConfigCatClient";

export const PROJECT_ID_ATTRIBUTE = "project_id";
export const TEAM_ID_ATTRIBUTE = "team_id";
export const TEAM_IDS_ATTRIBUTE = "team_ids";
export const TEAM_NAME_ATTRIBUTE = "team_name";
export const TEAM_NAMES_ATTRIBUTE = "team_names";

export 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();
}
}

export 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;
}
if (attributes.teams) {
custom[TEAM_NAMES_ATTRIBUTE] = attributes.teams.map((t) => t.name).join(",");
custom[TEAM_IDS_ATTRIBUTE] = attributes.teams.map((t) => t.id).join(",");
}

return new User(userId, email, "", custom);
}
32 changes: 32 additions & 0 deletions components/gitpod-protocol/src/experiments/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* 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 { Team } from "../teams-projects-protocol";

// Attributes define attributes which can be used to segment audiences.
// Set the attributes which you want to use to group audiences into.
export interface Attributes {
userId?: string;
email?: string;

// Currently selected Gitpod Project ID
projectId?: string;

// Currently selected Gitpod Team ID
teamId?: string;
// Currently selected Gitpod Team Name
teamName?: string;

// All the Gitpod Teams that the user is a member (or owner) of
teams?: Array<Team>;
}

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;
}
11 changes: 4 additions & 7 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ import { ClientMetadata, traceClientMetadata } from "../../../src/websocket/webs
import { BitbucketAppSupport } from "../bitbucket/bitbucket-app-support";
import { URL } from "url";
import { UserCounter } from "../user/user-counter";
import { getExperimentsClient } from "../../../src/experiments";
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";

@injectable()
export class GitpodServerEEImpl extends GitpodServerImpl {
Expand Down Expand Up @@ -1860,15 +1860,12 @@ export class GitpodServerEEImpl extends GitpodServerImpl {

protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise<void> {
const teams = await this.teamDB.findTeamsByUser(user.id);
const isUsageBasedBillingEnabled = await getExperimentsClient().getValueAsync(
const isUsageBasedBillingEnabled = await getExperimentsClientForBackend().getValueAsync(
"isUsageBasedBillingEnabled",
false,
{
identifier: user.id,
custom: {
team_ids: teams.map((t) => t.id).join(","),
team_names: teams.map((t) => t.name).join(","),
},
userId: user.id,
teams: teams,
},
);
if (!isUsageBasedBillingEnabled) {
Expand Down
23 changes: 0 additions & 23 deletions components/server/src/experiments.ts

This file was deleted.

14 changes: 0 additions & 14 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ 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";
import { Currency } from "@gitpod/gitpod-protocol/lib/plans";

// shortcut
Expand Down Expand Up @@ -2142,19 +2141,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
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
Loading