Skip to content

Commit f3bbcf7

Browse files
authored
Environments for Features (growthbook#236)
1 parent 8e2bf31 commit f3bbcf7

32 files changed

+547
-294
lines changed

packages/back-end/src/controllers/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export async function getExperimentConfig(
2727
const { key } = req.params;
2828

2929
try {
30-
const organization = await lookupOrganizationByApiKey(key);
30+
const { organization } = await lookupOrganizationByApiKey(key);
3131
if (!organization) {
3232
return res.status(400).json({
3333
status: 400,
@@ -89,7 +89,7 @@ export async function getExperimentsScript(
8989
const { key } = req.params;
9090

9191
try {
92-
const organization = await lookupOrganizationByApiKey(key);
92+
const { organization } = await lookupOrganizationByApiKey(key);
9393
if (!organization) {
9494
return res
9595
.status(400)

packages/back-end/src/controllers/features.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ export async function getFeaturesPublic(req: Request, res: Response) {
1717
const { key } = req.params;
1818

1919
try {
20-
const organization = await lookupOrganizationByApiKey(key);
20+
const { organization, environment } = await lookupOrganizationByApiKey(key);
2121
if (!organization) {
2222
return res.status(400).json({
2323
status: 400,
2424
error: "Invalid API key",
2525
});
2626
}
2727

28-
const features = await getFeatureDefinitions(organization);
28+
const features = await getFeatureDefinitions(organization, environment);
2929

3030
// Cache for 30 seconds, serve stale up to 1 hour (10 hours if origin is down)
3131
res.set(
@@ -69,6 +69,7 @@ export async function postFeatures(
6969
description: "",
7070
project: "",
7171
rules: [],
72+
environments: ["dev"],
7273
...otherProps,
7374
dateCreated: new Date(),
7475
dateUpdated: new Date(),
@@ -135,7 +136,8 @@ export async function putFeature(
135136
updates.defaultValue !== feature.defaultValue
136137
) {
137138
requiresWebhook = true;
138-
} else if ("rules" in updates) {
139+
}
140+
if ("rules" in updates) {
139141
if (updates.rules?.length !== feature.rules?.length) {
140142
requiresWebhook = true;
141143
} else {
@@ -153,6 +155,11 @@ export async function putFeature(
153155
});
154156
}
155157
}
158+
if ("environments" in updates) {
159+
if (updates.environments?.length !== feature.environments?.length) {
160+
requiresWebhook = true;
161+
}
162+
}
156163

157164
await updateFeature(feature.organization, id, {
158165
...updates,

packages/back-end/src/controllers/organizations.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,7 +1066,7 @@ export async function getApiKeys(req: AuthRequest, res: Response) {
10661066
}
10671067

10681068
export async function postApiKey(
1069-
req: AuthRequest<{ description?: string }>,
1069+
req: AuthRequest<{ description?: string; environment: string }>,
10701070
res: Response
10711071
) {
10721072
const { org } = getOrgFromReq(req);
@@ -1077,9 +1077,11 @@ export async function postApiKey(
10771077
});
10781078
}
10791079

1080+
const { description, environment } = req.body;
1081+
10801082
const { preferExisting } = req.query as { preferExisting?: string };
10811083
if (preferExisting) {
1082-
const existing = await getFirstApiKey(org.id);
1084+
const existing = await getFirstApiKey(org.id, environment);
10831085
if (existing) {
10841086
return res.status(200).json({
10851087
status: 200,
@@ -1088,9 +1090,7 @@ export async function postApiKey(
10881090
}
10891091
}
10901092

1091-
const { description } = req.body;
1092-
1093-
const key = await createApiKey(org.id, description);
1093+
const key = await createApiKey(org.id, environment, description);
10941094

10951095
res.status(200).json({
10961096
status: 200,

packages/back-end/src/jobs/webhooks.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export default function (ag: Agenda) {
2929
const { overrides, expIdMapping } = await getExperimentOverrides(
3030
webhook.organization
3131
);
32-
const features = await getFeatureDefinitions(webhook.organization);
32+
const features = await getFeatureDefinitions(
33+
webhook.organization,
34+
"production"
35+
);
3336
const payload = JSON.stringify({
3437
timestamp: Math.floor(Date.now() / 1000),
3538
overrides,

packages/back-end/src/models/ApiKeyModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const apiKeySchema = new mongoose.Schema({
66
type: String,
77
unique: true,
88
},
9+
environment: String,
910
description: String,
1011
organization: String,
1112
dateCreated: Date,

packages/back-end/src/models/FeatureModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const featureSchema = new mongoose.Schema({
1111
dateUpdated: Date,
1212
valueType: String,
1313
defaultValue: String,
14+
environments: [String],
1415
rules: [
1516
{
1617
_id: false,

packages/back-end/src/services/apiKey.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ import md5 from "md5";
44

55
export async function createApiKey(
66
organization: string,
7+
environment: string,
78
description?: string
89
): Promise<string> {
9-
const key = "key_" + md5(uniqid()).substr(0, 16);
10+
const envPrefix = environment === "production" ? "prod" : environment;
11+
12+
const key = "key_" + envPrefix + "_" + md5(uniqid()).substr(0, 16);
1013

1114
await ApiKeyModel.create({
1215
organization,
1316
key,
1417
description,
18+
environment,
1519
dateCreated: new Date(),
1620
});
1721

@@ -31,13 +35,14 @@ export async function deleteByOrganizationAndApiKey(
3135

3236
export async function lookupOrganizationByApiKey(
3337
key: string
34-
): Promise<string | null> {
38+
): Promise<{ organization?: string; environment?: string }> {
3539
const doc = await ApiKeyModel.findOne({
3640
key,
3741
});
3842

39-
if (!doc) return null;
40-
return doc.organization || null;
43+
if (!doc || !doc.organization) return {};
44+
const { organization, environment } = doc;
45+
return { organization, environment };
4146
}
4247

4348
export async function getAllApiKeysByOrganization(organization: string) {
@@ -46,8 +51,12 @@ export async function getAllApiKeysByOrganization(organization: string) {
4651
});
4752
}
4853

49-
export async function getFirstApiKey(organization: string) {
54+
export async function getFirstApiKey(
55+
organization: string,
56+
environment: string
57+
) {
5058
return ApiKeyModel.findOne({
5159
organization,
60+
environment,
5261
});
5362
}

packages/back-end/src/services/features.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,19 @@ function getJSONValue(type: FeatureValueType, value: string): any {
1717
if (type === "boolean") return value === "false" ? false : true;
1818
return null;
1919
}
20-
export async function getFeatureDefinitions(organization: string) {
20+
export async function getFeatureDefinitions(
21+
organization: string,
22+
environment?: string
23+
) {
2124
const features = await getAllFeatures(organization);
2225

2326
const defs: Record<string, FeatureDefinition> = {};
2427
features.forEach((feature) => {
28+
if (environment && !feature.environments?.includes(environment)) {
29+
defs[feature.id] = { defaultValue: null };
30+
return;
31+
}
32+
2533
defs[feature.id] = {
2634
defaultValue: getJSONValue(feature.valueType, feature.defaultValue),
2735
rules:

packages/back-end/types/apikey.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface ApiKeyInterface {
22
key: string;
3+
environment?: string;
34
description?: string;
45
organization: string;
56
dateCreated: Date;

packages/back-end/types/feature.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface FeatureInterface {
1010
dateCreated: Date;
1111
dateUpdated: Date;
1212
valueType: FeatureValueType;
13+
environments: string[];
1314
defaultValue: string;
1415
rules?: FeatureRule[];
1516
}

0 commit comments

Comments
 (0)