@terreno/feature-flags 57.2.0 → 57.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/package.json +4 -4
- package/src/flagConfiguration.test.ts +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @terreno/feature-flags
|
|
2
|
+
|
|
3
|
+
Feature flags and A/B testing plugin for @terreno/api.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @terreno/feature-flags
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependency: `mongoose ^8.0.0 || ^9.0.0`. `@terreno/api` is required at runtime.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import {TerrenoApp} from "@terreno/api";
|
|
17
|
+
import {AdminApp} from "@terreno/admin-backend";
|
|
18
|
+
import {FeatureFlagsApp, featureFlagAdminConfig} from "@terreno/feature-flags";
|
|
19
|
+
import {User} from "./models/user";
|
|
20
|
+
|
|
21
|
+
new TerrenoApp({userModel: User})
|
|
22
|
+
.register(new FeatureFlagsApp({}))
|
|
23
|
+
.register(new AdminApp({models: [featureFlagAdminConfig]}))
|
|
24
|
+
.start();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Authenticated clients load flags from `GET /feature-flags/flagConfiguration`. Prefer that bulk map over the deprecated `GET /feature-flags/evaluate` endpoint (responses include `Deprecation: true` and a `Sunset` header).
|
|
28
|
+
|
|
29
|
+
Pass `liveUpdates: {socketIoServer: io}` to broadcast flag-key changes over Socket.io. Live updates require MongoDB running as a replica set (a single-node replica set is enough).
|
|
30
|
+
|
|
31
|
+
## What's included
|
|
32
|
+
|
|
33
|
+
- `FeatureFlagsApp` — TerrenoPlugin for admin CRUD, `/flagConfiguration`, and `/evaluate`
|
|
34
|
+
- `FeatureFlag` — Mongoose model for flag documents
|
|
35
|
+
- `featureFlagAdminConfig` — ready-to-use `AdminApp` model config
|
|
36
|
+
- `evaluateFlag` / `evaluateAllFlags` — server-side evaluation helpers
|
|
37
|
+
- `MongoFeatureFlagProvider` — OpenFeature server provider
|
|
38
|
+
- Optional socket live updates on change streams
|
|
39
|
+
|
|
40
|
+
## Documentation
|
|
41
|
+
|
|
42
|
+
Full API reference: [docs/reference/feature-flags.md](https://github.com/flourishhealth/terreno/blob/master/docs/reference/feature-flags.md)
|
|
43
|
+
|
|
44
|
+
How-to: [Add feature flags](https://github.com/flourishhealth/terreno/blob/master/docs/how-to/add-feature-flags.md)
|
|
45
|
+
|
|
46
|
+
## License and Contributing
|
|
47
|
+
|
|
48
|
+
Licensed under the [MIT License](https://github.com/flourishhealth/terreno/blob/master/LICENSE). See [CONTRIBUTING.md](https://github.com/flourishhealth/terreno/blob/master/CONTRIBUTING.md) for contribution guidelines.
|
package/package.json
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@openfeature/server-sdk": "^1.22.0",
|
|
8
|
-
"@terreno/api": "57.
|
|
8
|
+
"@terreno/api": "57.3.0",
|
|
9
9
|
"express": "^5.2.1",
|
|
10
10
|
"lodash": "^4.18.1"
|
|
11
11
|
},
|
|
12
|
-
"description": "Feature flags and A/B testing plugin for @terreno/api",
|
|
12
|
+
"description": "Feature flags and A/B testing plugin for @terreno/api — part of Terreno, the batteries-included TypeScript framework for universal apps.",
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@biomejs/biome": "^2.3.6",
|
|
15
|
-
"@terreno/test": "57.
|
|
15
|
+
"@terreno/test": "57.3.0",
|
|
16
16
|
"@types/bun": "^1.4.0",
|
|
17
17
|
"@types/express": "^5.0.6",
|
|
18
18
|
"@types/lodash": "^4.17.17",
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"test:coverage": "bun run ../scripts/check-coverage.ts"
|
|
55
55
|
},
|
|
56
56
|
"types": "dist/index.d.ts",
|
|
57
|
-
"version": "57.
|
|
57
|
+
"version": "57.3.0"
|
|
58
58
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {describe, expect, it} from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {buildFlagDefinition, effectiveDefaultVariantForFlag} from "./flagConfiguration";
|
|
4
|
+
import type {FeatureFlagDocument, SegmentFunction} from "./types";
|
|
5
|
+
|
|
6
|
+
const booleanFlag = (overrides: Partial<FeatureFlagDocument> = {}): FeatureFlagDocument =>
|
|
7
|
+
({
|
|
8
|
+
archived: false,
|
|
9
|
+
description: "d",
|
|
10
|
+
enabled: true,
|
|
11
|
+
key: "bool",
|
|
12
|
+
name: "Bool",
|
|
13
|
+
rolloutPercentage: 100,
|
|
14
|
+
rules: [],
|
|
15
|
+
type: "boolean",
|
|
16
|
+
variants: [],
|
|
17
|
+
...overrides,
|
|
18
|
+
}) as unknown as FeatureFlagDocument;
|
|
19
|
+
|
|
20
|
+
const variantFlag = (overrides: Partial<FeatureFlagDocument> = {}): FeatureFlagDocument =>
|
|
21
|
+
({
|
|
22
|
+
archived: false,
|
|
23
|
+
description: "d",
|
|
24
|
+
enabled: true,
|
|
25
|
+
key: "exp",
|
|
26
|
+
name: "Exp",
|
|
27
|
+
rolloutPercentage: 100,
|
|
28
|
+
rules: [],
|
|
29
|
+
type: "variant",
|
|
30
|
+
variants: [
|
|
31
|
+
{key: "control", weight: 50},
|
|
32
|
+
{key: "treatment", weight: 50},
|
|
33
|
+
],
|
|
34
|
+
...overrides,
|
|
35
|
+
}) as unknown as FeatureFlagDocument;
|
|
36
|
+
|
|
37
|
+
const noSegments: Record<string, SegmentFunction> = {};
|
|
38
|
+
|
|
39
|
+
describe("effectiveDefaultVariantForFlag", () => {
|
|
40
|
+
it("uses defaultVariant when present", () => {
|
|
41
|
+
expect(effectiveDefaultVariantForFlag(booleanFlag({defaultVariant: "on"}))).toBe("on");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("defaults boolean flags to off", () => {
|
|
45
|
+
expect(effectiveDefaultVariantForFlag(booleanFlag())).toBe("off");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("defaults variant flags to the first variant key", () => {
|
|
49
|
+
expect(effectiveDefaultVariantForFlag(variantFlag())).toBe("control");
|
|
50
|
+
expect(effectiveDefaultVariantForFlag(variantFlag({variants: []}))).toBe("off");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("buildFlagDefinition", () => {
|
|
55
|
+
it("maps a boolean evaluation to on/off variants", () => {
|
|
56
|
+
expect(buildFlagDefinition(booleanFlag(), "user-1", {}, noSegments)).toEqual({
|
|
57
|
+
defaultVariant: "on",
|
|
58
|
+
disabled: false,
|
|
59
|
+
variants: {off: false, on: true},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("maps a disabled variant flag onto the default variant", () => {
|
|
64
|
+
const definition = buildFlagDefinition(variantFlag({enabled: false}), "user-1", {}, noSegments);
|
|
65
|
+
expect(definition.defaultVariant).toBe("control");
|
|
66
|
+
expect(definition.variants).toEqual({control: "control", treatment: "treatment"});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("uses the evaluated variant key when it matches a defined variant", () => {
|
|
70
|
+
const definition = buildFlagDefinition(
|
|
71
|
+
variantFlag({
|
|
72
|
+
rules: [{field: "id", operator: "eq", value: "user-1", variant: "treatment"}],
|
|
73
|
+
}),
|
|
74
|
+
"user-1",
|
|
75
|
+
{id: "user-1"},
|
|
76
|
+
noSegments
|
|
77
|
+
);
|
|
78
|
+
expect(definition.defaultVariant).toBe("treatment");
|
|
79
|
+
});
|
|
80
|
+
});
|