Skip to content

Commit

Permalink
feat: add segments (#1426)
Browse files Browse the repository at this point in the history
* refactor: fix missing tsconfig path in .eslintrc

* refactor: require contextName and operator

* refactor: fix crash on missing feature strategies

* feat: add segments schema

* feat: add segments client API

* feat: add segments permissions

* refactor: fail migration if things exist

* refactor: remove strategy IDs from responses

* refactor: allow empty description

* refactor: add segment import/export

* refactor: add perf scripts

* refactor: add get segment fn

* refactor: move constraint validation endpoint

* refactor: use a separate id for segment updates

* refactor: use PERF_AUTH_KEY for artillery

* refactor: adjust segment seed size

* refactor: add missing event data await

* refactor: improve method order

* refactor: remove request body limit override
  • Loading branch information
olav committed Mar 29, 2022
1 parent f469b41 commit 66d9d7a
Show file tree
Hide file tree
Showing 50 changed files with 1,408 additions and 98 deletions.
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -43,6 +43,8 @@
"test:watch": "yarn test --watch",
"test:coverage": "NODE_ENV=test PORT=4243 jest --coverage --forceExit --testTimeout=10000",
"test:coverage:jest": "NODE_ENV=test PORT=4243 jest --silent --ci --json --coverage --testLocationInResults --outputFile=\"report.json\" --forceExit --testTimeout=10000",
"seed:setup": "ts-node src/test/e2e/seed/segment.seed.ts",
"seed:serve": "UNLEASH_DATABASE_NAME=unleash_test UNLEASH_DATABASE_SCHEMA=seed yarn run start:dev",
"clean": "del-cli --force dist"
},
"jest": {
Expand Down
32 changes: 32 additions & 0 deletions perf/README.md
@@ -0,0 +1,32 @@
# /perf

Testing performance testing! Files of note:

```shell
# Configure the app URL and auth token to use in performance testing.
./env.sh

# Export all the data from the app at the configured URL.
./seed/export.sh

# Import previously exported data to the app instance.
./seed/import.sh

# Measure the GZIP response size for interesting endpoints.
./test/gzip.sh

# Run a few load test scenarios against the app.
./test/artillery.sh
```

See also the following scripts in `package.json`:

```shell
# Fill the unleash_testing/seed schema with seed data.
$ yarn seed:setup

# Serve the unleash_testing/seed schema data, for exports.
$ yarn seed:serve
```

Edit files in `/test/e2e/seed` to change the amount data.
4 changes: 4 additions & 0 deletions perf/env.sh
@@ -0,0 +1,4 @@
#!/usr/bin/env bash

export PERF_AUTH_KEY="*:*.964a287e1b728cb5f4f3e0120df92cb5"
export PERF_APP_URL="http://localhost:4242"
1 change: 1 addition & 0 deletions perf/seed/.gitignore
@@ -0,0 +1 @@
/export.json
12 changes: 12 additions & 0 deletions perf/seed/export.sh
@@ -0,0 +1,12 @@
#!/usr/bin/env bash

set -feu

cd "$(dirname "$0")"

. ../env.sh

# Export data. Delete environments since they can't be imported.
curl -H "Authorization: $PERF_AUTH_KEY" "$PERF_APP_URL/api/admin/state/export" \
| jq 'del(.environments)' \
> export.json
13 changes: 13 additions & 0 deletions perf/seed/import.sh
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

set -feu

cd "$(dirname "$0")"

. ../env.sh

curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: $PERF_AUTH_KEY" \
-d @export.json \
"$PERF_APP_URL/api/admin/state/import?drop=true&keep=false"
2 changes: 2 additions & 0 deletions perf/test/.gitignore
@@ -0,0 +1,2 @@
/artillery.json
/artillery.json.html
13 changes: 13 additions & 0 deletions perf/test/artillery.sh
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

set -feu

cd "$(dirname "$0")"

. ../env.sh

artillery run ./artillery.yaml --output artillery.json

artillery report artillery.json

echo "See artillery.json.html for results"
12 changes: 12 additions & 0 deletions perf/test/artillery.yaml
@@ -0,0 +1,12 @@
config:
target: "http://localhost:4242"
defaults:
headers:
authorization: "{{ $processEnvironment.PERF_AUTH_KEY }}"
phases:
- duration: 60
arrivalRate: 10
scenarios:
- flow:
- get:
url: "/api/client/features"
25 changes: 25 additions & 0 deletions perf/test/gzip.sh
@@ -0,0 +1,25 @@
#!/usr/bin/env bash

set -feu

cd "$(dirname "$0")"

. ../env.sh

print_response_size () {
local URL
local RES
URL="$1"
RES="$(curl -s -H "Authorization: $PERF_AUTH_KEY" "$URL")"
echo
echo "$URL"
echo
echo "* Byte size: $(echo "$RES" | wc -c) bytes"
echo "* GZIP size: $(echo "$RES" | gzip -6 | wc -c) bytes"
}

print_response_size "$PERF_APP_URL/api/admin/projects"

print_response_size "$PERF_APP_URL/api/admin/features"

print_response_size "$PERF_APP_URL/api/client/features"
29 changes: 26 additions & 3 deletions src/lib/db/feature-strategy-store.ts
Expand Up @@ -14,6 +14,7 @@ import {
IStrategyConfig,
} from '../types/model';
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
import { PartialSome } from '../types/partial';

const COLUMNS = [
'id',
Expand All @@ -36,6 +37,7 @@ const mapperToColumnNames = {
const T = {
features: 'features',
featureStrategies: 'feature_strategies',
featureStrategySegment: 'feature_strategy_segment',
featureEnvs: 'feature_environments',
};

Expand Down Expand Up @@ -128,7 +130,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {

async exists(key: string): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS (SELECT 1 FROM ${T.featureStrategies} WHERE id = ?) AS present`,
`SELECT EXISTS(SELECT 1 FROM ${T.featureStrategies} WHERE id = ?) AS present`,
[key],
);
const { present } = result.rows[0];
Expand All @@ -148,9 +150,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
}

async createStrategyFeatureEnv(
strategyConfig: Omit<IFeatureStrategy, 'id' | 'createdAt'>,
strategyConfig: PartialSome<IFeatureStrategy, 'id' | 'createdAt'>,
): Promise<IFeatureStrategy> {
const strategyRow = mapInput({ ...strategyConfig, id: uuidv4() });
const strategyRow = mapInput({ id: uuidv4(), ...strategyConfig });
const rows = await this.db<IFeatureStrategiesTable>(T.featureStrategies)
.insert(strategyRow)
.returning('*');
Expand Down Expand Up @@ -422,6 +424,27 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
.where({ feature_name: featureName })
.update({ project_name: newProjectId });
}

async getStrategiesBySegment(
segmentId: number,
): Promise<IFeatureStrategy[]> {
const stopTimer = this.timer('getStrategiesBySegment');
const rows = await this.db
.select(this.prefixColumns())
.from<IFeatureStrategiesTable>(T.featureStrategies)
.join(
T.featureStrategySegment,
`${T.featureStrategySegment}.feature_strategy_id`,
`${T.featureStrategies}.id`,
)
.where(`${T.featureStrategySegment}.segment_id`, '=', segmentId);
stopTimer();
return rows.map(mapRow);
}

prefixColumns(): string[] {
return COLUMNS.map((c) => `${T.featureStrategies}.${c}`);
}
}

module.exports = FeatureStrategiesStore;
Expand Down

0 comments on commit 66d9d7a

Please sign in to comment.