Skip to content

Commit

Permalink
first
Browse files Browse the repository at this point in the history
  • Loading branch information
mfrachet committed Oct 14, 2024
0 parents commit 82b938c
Show file tree
Hide file tree
Showing 21 changed files with 3,869 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .github/actions/monorepo/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: "Monorepo setup"
description: "Install deps + dotenv setup"
runs:
using: "composite"
steps:
- uses: actions/setup-node@v2
name: Install Node 20
with:
node-version: "20"

- uses: ./.github/actions/pnpm
29 changes: 29 additions & 0 deletions .github/actions/pnpm/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: "Pnpm setup"
description: "Handle caching and pnpm resolution"
runs:
using: "composite"
steps:
- uses: actions/setup-node@v2
name: Install Node 20
with:
node-version: "20"

- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
run_install: false

- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
27 changes: 27 additions & 0 deletions .github/workflows/core.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Core

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
shared:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "20"

- uses: ./.github/actions/monorepo

- name: Install dependencies
shell: bash
run: pnpm install

- name: Shared CI checks
run: pnpm run ci
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.


#output
dist

# dependencies
node_modules
/.pnp
.pnp.js

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
*/schema.prisma

.turbo


# tailwind generated styles on build
.next
.netlify
websites/frontend/app/styles/app.css
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v20.18.0
145 changes: 145 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<div align="center">⛵ A feature flags evaluation engine, runtime agnostic with no remote services.</div>
<br/>

![20 pixelized boats on the sea](https://github.com/user-attachments/assets/5628ad4c-6e77-4f5c-9e81-2bc5f14b5d51)


---

## What is Flag Engine?

Flag Engine is a runtime agnostic and source agnostic feature flags evaluation engine. You give it a configuration and a context, and it will evaluate the flags for you.

**What Flag Engine is not:**

- A feature flag management platform
- A feature flag dashboard
- An analytics platform, you have to send your events to your analytics platform
- A way to store your feature flags
- A drop-in replacement for [OpenFeature](https://openfeature.dev). (a provider for Flag Engine will be created to be compliant with the OpenFeature API)

## Usage

1. Install the package:

```bash
$ pnpm add @flag-engine/core
```

2. Create a configuration (or build it from where it makes sense for you like a DB or a static file, or whatever)

```typescript
import { createFlagEngine } from "@flag-engine/core";

const flagsConfig: FlagsConfiguration = [
{
key: "feature-flag-key",
status: "enabled", // the status of the flag, can be "enabled" or "disabled"
strategies: [], // a set of condition for customization purpose
},
];

// This is useful to create conditions based on the user's attributes.
// The __id is mandatory and a special one that will be used to compute % based variants.
const userConfiguration: UserConfiguration = {
__id: "73a56693-0f83-4ffc-a61d-7c95fdf68693", // a unique identifier for the user or an empty string if the users are not connected.
};

const engine = createFlagEngine(flagsConfig, userConfiguration);

// Evaluate one specific feature flag
const isFlagEnabled = engine.evaluate("feature-flag-key"); // true

// Evaluate all the feature flags at once
const allFlags = engine.evaluateAll(); // { "feature-flag-key": true }
```

## Concepts

### Flag configuration

It's a descriptive object that contains guidance on how the feature flag should be evaluated. It's composed of a list of feature flags with their **status** (`enabled` or `disabled`) and a list of **strategies**.

### User configuration

This is an object that holds details about the current user. It includes a unique identifier (`__id`, which is mandatory) and other custom attributes (defined by you) that can be used to evaluate feature flags. These attributes can be utilized within strategies to specify the conditions necessary to determine a computed feature flag variant.

This is useful if you want your QA team to test the feature behind the flag: you can create a strategy that targets users with your domain address (e.g., `@gmail.com`), ensuring that only they will see the flag enabled.

> Another example: the current user has a `country` attribute with a value of `France`. I have defined a strategy with a condition on the `country` attribute with a value of `France`. This user is eligible to resolve a computed variant. If the user sends a `US` country, they will resolve a `false` variant.
**Notes**:

- the `__id` is mandatory and should be your user uniquer id OR an empty string if the users are not connected.

### Flag status

- `enabled`: The feature flag is enabled. (returns true or the computed variant)
- `disabled`: The feature flag is disabled. (returns false every time)

### Strategies

**Strategies** is where all the customization stands. In a strategy, you can define:

- **a set of rules** that are needed to be eligible for the feature flag evaluation (using the user configuration/context)
- a list of variants with the percentage of the population that should see each variant. Those are computed against the `__id` of the user (this is why it's mandatory).

**It's important to understand that:**

- Each **strategy** is an `or`. It means that if the user matches **at least one strategy**, they will be eligible for the feature flag evaluation.

- Each **rule** is an `and`. It means that **all the rules in one strategy must be true** for the user for the strategy to be eligible.

This is convenient for combining **and** and **or** logic and create complex conditional feature flags.

## An exhaustive example

I want to show my audience **2 variants** of a feature. Only the people living in `France` and `Spain` should see the feature.

Here is how I can do that:

```typescript
const flagsConfig: FlagsConfiguration = [
{
key: "feature-flag-key",
status: "enabled", // the status of the flag, can be "enabled" or "disabled"
strategies: [
{
name: "only-france-and-spain",
rules: [
{
field: "country",
operator: "in",
value: ["France", "Spain"],
},
],
variants: [
{
name: "A",
percent: 50,
},
{
name: "B",
percent: 50,
},
],
},
],
},
];

const machine = createFlagEngine(flagsConfig, {
__id: "b",
country: "France",
});

const variant = machine.evaluate("feature-flag-key"); // gives back B
```

Now, I suggest you give it a try, build your config object the way you prefer and start building stuff!

❤️❤️❤️

---

Built by [@mfrachet](https://twitter.com/mfrachet)
21 changes: 21 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "flag-engine",
"version": "1.0.0",
"description": "",
"private": true,
"packageManager": "[email protected]",
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"lint": "turbo lint",
"test": "turbo test",
"bundlesize": "turbo bundlesize",
"ci": "CI=true turbo run lint test bundlesize"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"turbo": "^2.1.2"
}
}
3 changes: 3 additions & 0 deletions packages/core/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
src
node_modules
.turbo
12 changes: 12 additions & 0 deletions packages/core/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @ts-check

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["dist"],
}
);
46 changes: 46 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@flag-engine/core",
"private": false,
"version": "0.0.1",
"description": "Feature flags evaluation engine, runtime agnostic",
"type": "module",
"main": "./dist/index.cjs.js",
"module": "./dist/index.mjs",
"exports": {
".": {
"require": "./dist/index.cjs.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
},
"types": "./dist/index.d.ts",
"scripts": {
"build": "rollup -c rollup.config.mjs",
"start": "tsx src/index.ts",
"test": "vitest",
"coverage": "vitest run --coverage",
"lint": "eslint ."
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.12.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.0",
"@types/eslint__js": "^8.42.3",
"@types/murmurhash-js": "^1.0.6",
"@vitest/coverage-v8": "2.1.2",
"eslint": "^9.12.0",
"rollup": "^4.24.0",
"tslib": "^2.7.0",
"tsx": "^4.19.1",
"typescript": "^5.6.2",
"typescript-eslint": "^8.8.1",
"vitest": "^2.1.2"
},
"dependencies": {
"murmurhash-js": "^1.0.0"
}
}
33 changes: 33 additions & 0 deletions packages/core/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import typescript from "@rollup/plugin-typescript";
import terser from "@rollup/plugin-terser";
import { nodeResolve } from "@rollup/plugin-node-resolve";

const external = ["murmurhash-js"];
const globals = { "murmurhash-js": "murmurhash-js" };

export default () => {
return {
input: "src/index.ts",
output: [
{
file: "dist/index.cjs.js",
format: "cjs",
name: "ff-engine",
globals,
},
{
file: "dist/index.mjs",
format: "es",
},
],
plugins: [
nodeResolve(),
typescript({
tsconfig: "./tsconfig.json",
sourceMap: true,
}),
terser(),
],
external,
};
};
Loading

0 comments on commit 82b938c

Please sign in to comment.