@naturalcycles/abba 1.12.1 → 1.13.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/dist/abba.js +2 -2
- package/dist/dao/experiment.dao.d.ts +1 -1
- package/dist/migrations/init.sql +2 -0
- package/dist/types.d.ts +19 -8
- package/dist/types.js +9 -0
- package/dist/util.d.ts +5 -1
- package/dist/util.js +12 -4
- package/package.json +6 -6
- package/readme.md +2 -0
- package/src/abba.ts +8 -3
- package/src/migrations/init.sql +2 -0
- package/src/types.ts +15 -4
- package/src/util.ts +13 -4
package/dist/abba.js
CHANGED
|
@@ -110,7 +110,7 @@ class Abba {
|
|
|
110
110
|
if (existingOnly)
|
|
111
111
|
return null;
|
|
112
112
|
const experiment = await this.experimentDao.requireById(experimentId);
|
|
113
|
-
if (
|
|
113
|
+
if (!(0, util_1.canGenerateNewAssignments)(experiment))
|
|
114
114
|
return null;
|
|
115
115
|
(0, js_lib_1._assert)(segmentationData, 'Segmentation data required when creating a new assignment');
|
|
116
116
|
const buckets = await this.bucketDao.getBy('experimentId', experimentId);
|
|
@@ -149,7 +149,7 @@ class Abba {
|
|
|
149
149
|
bucketKey: experiment.buckets.find(b => b.id === existing.bucketId)?.key || null,
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
|
-
else if (!existingOnly &&
|
|
152
|
+
else if (!existingOnly && (0, util_1.canGenerateNewAssignments)(experiment)) {
|
|
153
153
|
const assignment = this.generateUserAssignmentData(experiment, userId, segmentationData);
|
|
154
154
|
if (assignment) {
|
|
155
155
|
const created = this.userAssignmentDao.create(assignment);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CommonDao, CommonDB } from '@naturalcycles/db-lib';
|
|
2
2
|
import { Saved } from '@naturalcycles/js-lib';
|
|
3
3
|
import { BaseExperiment, Experiment } from '../types';
|
|
4
|
-
|
|
4
|
+
type ExperimentDBM = Saved<BaseExperiment> & {
|
|
5
5
|
rules: string | null;
|
|
6
6
|
};
|
|
7
7
|
export declare class ExperimentDao extends CommonDao<Experiment, ExperimentDBM> {
|
package/dist/migrations/init.sql
CHANGED
|
@@ -16,6 +16,8 @@ CREATE TABLE IF NOT EXISTS `Experiment` (
|
|
|
16
16
|
`status` INTEGER NOT NULL,
|
|
17
17
|
`sampling` INTEGER NOT NULL,
|
|
18
18
|
`description` VARCHAR(240) NULL,
|
|
19
|
+
`startDateIncl` DATE NOT NULL,
|
|
20
|
+
`endDateExcl` DATE NOT NULL,
|
|
19
21
|
`created` INTEGER(11) NOT NULL,
|
|
20
22
|
`updated` INTEGER(11) NOT NULL,
|
|
21
23
|
`rules` JSON NULL,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { CommonDB } from '@naturalcycles/db-lib';
|
|
2
|
-
import { BaseDBEntity, Saved } from '@naturalcycles/js-lib';
|
|
2
|
+
import { BaseDBEntity, IsoDateString, Saved } from '@naturalcycles/js-lib';
|
|
3
3
|
export interface AbbaConfig {
|
|
4
4
|
db: CommonDB;
|
|
5
5
|
}
|
|
6
|
-
export
|
|
6
|
+
export type BaseExperiment = BaseDBEntity<number> & {
|
|
7
7
|
id: number;
|
|
8
8
|
status: number;
|
|
9
9
|
sampling: number;
|
|
10
10
|
description: string | null;
|
|
11
|
+
startDateIncl: IsoDateString;
|
|
12
|
+
endDateExcl: IsoDateString;
|
|
11
13
|
};
|
|
12
|
-
export
|
|
14
|
+
export type Experiment = BaseExperiment & {
|
|
13
15
|
rules: SegmentationRule[];
|
|
14
16
|
};
|
|
15
|
-
export
|
|
17
|
+
export type ExperimentWithBuckets = Saved<Experiment> & {
|
|
16
18
|
buckets: Saved<Bucket>[];
|
|
17
19
|
};
|
|
18
20
|
export interface BucketInput {
|
|
@@ -20,23 +22,32 @@ export interface BucketInput {
|
|
|
20
22
|
key: string;
|
|
21
23
|
ratio: number;
|
|
22
24
|
}
|
|
23
|
-
export
|
|
25
|
+
export type Bucket = BaseDBEntity<number> & {
|
|
24
26
|
experimentId: number;
|
|
25
27
|
key: string;
|
|
26
28
|
ratio: number;
|
|
27
29
|
};
|
|
28
|
-
export
|
|
30
|
+
export type UserAssignment = BaseDBEntity<number> & {
|
|
29
31
|
userId: string;
|
|
30
32
|
experimentId: number;
|
|
31
33
|
bucketId: number | null;
|
|
32
34
|
};
|
|
33
|
-
export
|
|
35
|
+
export type GeneratedUserAssignment = Saved<UserAssignment> & {
|
|
34
36
|
bucketKey: string | null;
|
|
35
37
|
};
|
|
36
|
-
export
|
|
38
|
+
export type SegmentationData = Record<string, string | boolean | number>;
|
|
37
39
|
export declare enum AssignmentStatus {
|
|
40
|
+
/**
|
|
41
|
+
* Will return existing assignments and generate new assignments
|
|
42
|
+
*/
|
|
38
43
|
Active = 1,
|
|
44
|
+
/**
|
|
45
|
+
* Will return existing assignments but not generate new assignments
|
|
46
|
+
*/
|
|
39
47
|
Paused = 2,
|
|
48
|
+
/**
|
|
49
|
+
* Will not return any assignments
|
|
50
|
+
*/
|
|
40
51
|
Inactive = 3
|
|
41
52
|
}
|
|
42
53
|
export interface SegmentationRule {
|
package/dist/types.js
CHANGED
|
@@ -3,7 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.AssignmentStatus = void 0;
|
|
4
4
|
var AssignmentStatus;
|
|
5
5
|
(function (AssignmentStatus) {
|
|
6
|
+
/**
|
|
7
|
+
* Will return existing assignments and generate new assignments
|
|
8
|
+
*/
|
|
6
9
|
AssignmentStatus[AssignmentStatus["Active"] = 1] = "Active";
|
|
10
|
+
/**
|
|
11
|
+
* Will return existing assignments but not generate new assignments
|
|
12
|
+
*/
|
|
7
13
|
AssignmentStatus[AssignmentStatus["Paused"] = 2] = "Paused";
|
|
14
|
+
/**
|
|
15
|
+
* Will not return any assignments
|
|
16
|
+
*/
|
|
8
17
|
AssignmentStatus[AssignmentStatus["Inactive"] = 3] = "Inactive";
|
|
9
18
|
})(AssignmentStatus = exports.AssignmentStatus || (exports.AssignmentStatus = {}));
|
package/dist/util.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Saved } from '@naturalcycles/js-lib';
|
|
2
|
-
import { Bucket, SegmentationData, SegmentationRule } from './types';
|
|
2
|
+
import { Bucket, Experiment, SegmentationData, SegmentationRule } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Generate a random number between 0 and 100
|
|
5
5
|
*/
|
|
@@ -28,3 +28,7 @@ export declare const validateSegmentationRules: (rules: SegmentationRule[], segm
|
|
|
28
28
|
* Validate a users segmentation data against a single rule
|
|
29
29
|
*/
|
|
30
30
|
export declare const validateSegmentationRule: (rule: SegmentationRule, data: SegmentationData) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Returns true if an experiment is able to generate new assignments based on status and start/end dates
|
|
33
|
+
*/
|
|
34
|
+
export declare const canGenerateNewAssignments: (experiment: Experiment) => boolean;
|
package/dist/util.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validateSegmentationRule = exports.validateSegmentationRules = exports.validateTotalBucketRatio = exports.determineBucket = exports.determineAssignment = exports.rollDie = void 0;
|
|
3
|
+
exports.canGenerateNewAssignments = exports.validateSegmentationRule = exports.validateSegmentationRules = exports.validateTotalBucketRatio = exports.determineBucket = exports.determineAssignment = exports.rollDie = void 0;
|
|
4
|
+
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
4
5
|
const semver_1 = require("semver");
|
|
6
|
+
const types_1 = require("./types");
|
|
5
7
|
/**
|
|
6
8
|
* Generate a random number between 0 and 100
|
|
7
9
|
*/
|
|
@@ -89,8 +91,14 @@ const validateSegmentationRule = (rule, data) => {
|
|
|
89
91
|
else if (operator === 'boolean') {
|
|
90
92
|
return Boolean(value) === data[key];
|
|
91
93
|
}
|
|
92
|
-
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
94
|
+
return false;
|
|
95
95
|
};
|
|
96
96
|
exports.validateSegmentationRule = validateSegmentationRule;
|
|
97
|
+
/**
|
|
98
|
+
* Returns true if an experiment is able to generate new assignments based on status and start/end dates
|
|
99
|
+
*/
|
|
100
|
+
const canGenerateNewAssignments = (experiment) => {
|
|
101
|
+
return (experiment.status === types_1.AssignmentStatus.Active &&
|
|
102
|
+
(0, js_lib_1.localDate)().isBetween(experiment.startDateIncl, experiment.endDateExcl, '[)'));
|
|
103
|
+
};
|
|
104
|
+
exports.canGenerateNewAssignments = canGenerateNewAssignments;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/abba",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"prepare": "husky install",
|
|
6
6
|
"build": "build",
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"semver": "^7.3.5"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@naturalcycles/dev-lib": "^
|
|
17
|
-
"@types/node": "^
|
|
16
|
+
"@naturalcycles/dev-lib": "^13.15.0",
|
|
17
|
+
"@types/node": "^18.11.18",
|
|
18
18
|
"@types/semver": "^7.3.9",
|
|
19
|
-
"jest": "^
|
|
19
|
+
"jest": "^29.3.1"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
@@ -29,14 +29,14 @@
|
|
|
29
29
|
"main": "dist/index.js",
|
|
30
30
|
"types": "dist/index.d.ts",
|
|
31
31
|
"publishConfig": {
|
|
32
|
-
"access": "
|
|
32
|
+
"access": "restricted"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
36
36
|
"url": "https://github.com/NaturalCycles/abba"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
39
|
+
"node": ">=18.12.0"
|
|
40
40
|
},
|
|
41
41
|
"description": "AB test assignment configuration tool for Node.js",
|
|
42
42
|
"author": "Natural Cycles Team",
|
package/readme.md
CHANGED
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
- **Sampling:** Restrictions on what proportion of the target audience will be involved in the
|
|
40
40
|
experiment
|
|
41
41
|
- **Bucket:** An allocation that defines what variant of a particular experience the user will have
|
|
42
|
+
- **Start/End dates:** The timeframe that assignments will be generated for this experiment when
|
|
43
|
+
active
|
|
42
44
|
|
|
43
45
|
<!-- BUILTWITH -->
|
|
44
46
|
|
package/src/abba.ts
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
GeneratedUserAssignment,
|
|
11
11
|
UserAssignment,
|
|
12
12
|
} from './types'
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
canGenerateNewAssignments,
|
|
15
|
+
determineAssignment,
|
|
16
|
+
validateSegmentationRules,
|
|
17
|
+
validateTotalBucketRatio,
|
|
18
|
+
} from './util'
|
|
14
19
|
import { ExperimentDao, experimentDao } from './dao/experiment.dao'
|
|
15
20
|
import { UserAssignmentDao, userAssignmentDao } from './dao/userAssignment.dao'
|
|
16
21
|
import { BucketDao, bucketDao } from './dao/bucket.dao'
|
|
@@ -161,7 +166,7 @@ export class Abba {
|
|
|
161
166
|
if (existingOnly) return null
|
|
162
167
|
|
|
163
168
|
const experiment = await this.experimentDao.requireById(experimentId)
|
|
164
|
-
if (experiment
|
|
169
|
+
if (!canGenerateNewAssignments(experiment)) return null
|
|
165
170
|
|
|
166
171
|
_assert(segmentationData, 'Segmentation data required when creating a new assignment')
|
|
167
172
|
|
|
@@ -214,7 +219,7 @@ export class Abba {
|
|
|
214
219
|
...existing,
|
|
215
220
|
bucketKey: experiment.buckets.find(b => b.id === existing.bucketId)?.key || null,
|
|
216
221
|
})
|
|
217
|
-
} else if (!existingOnly && experiment
|
|
222
|
+
} else if (!existingOnly && canGenerateNewAssignments(experiment)) {
|
|
218
223
|
const assignment = this.generateUserAssignmentData(experiment, userId, segmentationData)
|
|
219
224
|
if (assignment) {
|
|
220
225
|
const created = this.userAssignmentDao.create(assignment)
|
package/src/migrations/init.sql
CHANGED
|
@@ -16,6 +16,8 @@ CREATE TABLE IF NOT EXISTS `Experiment` (
|
|
|
16
16
|
`status` INTEGER NOT NULL,
|
|
17
17
|
`sampling` INTEGER NOT NULL,
|
|
18
18
|
`description` VARCHAR(240) NULL,
|
|
19
|
+
`startDateIncl` DATE NOT NULL,
|
|
20
|
+
`endDateExcl` DATE NOT NULL,
|
|
19
21
|
`created` INTEGER(11) NOT NULL,
|
|
20
22
|
`updated` INTEGER(11) NOT NULL,
|
|
21
23
|
`rules` JSON NULL,
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CommonDB } from '@naturalcycles/db-lib'
|
|
2
|
-
import { BaseDBEntity, Saved } from '@naturalcycles/js-lib'
|
|
2
|
+
import { BaseDBEntity, IsoDateString, Saved } from '@naturalcycles/js-lib'
|
|
3
3
|
|
|
4
4
|
export interface AbbaConfig {
|
|
5
5
|
db: CommonDB
|
|
@@ -18,6 +18,8 @@ export type BaseExperiment = BaseDBEntity<number> & {
|
|
|
18
18
|
status: number
|
|
19
19
|
sampling: number
|
|
20
20
|
description: string | null
|
|
21
|
+
startDateIncl: IsoDateString
|
|
22
|
+
endDateExcl: IsoDateString
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export type Experiment = BaseExperiment & {
|
|
@@ -53,9 +55,18 @@ export type GeneratedUserAssignment = Saved<UserAssignment> & {
|
|
|
53
55
|
export type SegmentationData = Record<string, string | boolean | number>
|
|
54
56
|
|
|
55
57
|
export enum AssignmentStatus {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Will return existing assignments and generate new assignments
|
|
60
|
+
*/
|
|
61
|
+
Active = 1,
|
|
62
|
+
/**
|
|
63
|
+
* Will return existing assignments but not generate new assignments
|
|
64
|
+
*/
|
|
65
|
+
Paused = 2,
|
|
66
|
+
/**
|
|
67
|
+
* Will not return any assignments
|
|
68
|
+
*/
|
|
69
|
+
Inactive = 3,
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
export interface SegmentationRule {
|
package/src/util.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Saved } from '@naturalcycles/js-lib'
|
|
1
|
+
import { localDate, Saved } from '@naturalcycles/js-lib'
|
|
2
2
|
import { satisfies } from 'semver'
|
|
3
|
-
import { Bucket, SegmentationData, SegmentationRule } from './types'
|
|
3
|
+
import { AssignmentStatus, Bucket, Experiment, SegmentationData, SegmentationRule } from './types'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Generate a random number between 0 and 100
|
|
@@ -92,7 +92,16 @@ export const validateSegmentationRule = (
|
|
|
92
92
|
return new RegExp(value.toString()).test(data[key]?.toString() || '')
|
|
93
93
|
} else if (operator === 'boolean') {
|
|
94
94
|
return Boolean(value) === data[key]
|
|
95
|
-
} else {
|
|
96
|
-
return false
|
|
97
95
|
}
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns true if an experiment is able to generate new assignments based on status and start/end dates
|
|
101
|
+
*/
|
|
102
|
+
export const canGenerateNewAssignments = (experiment: Experiment): boolean => {
|
|
103
|
+
return (
|
|
104
|
+
experiment.status === AssignmentStatus.Active &&
|
|
105
|
+
localDate().isBetween(experiment.startDateIncl, experiment.endDateExcl, '[)')
|
|
106
|
+
)
|
|
98
107
|
}
|