@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 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 (experiment.status !== types_1.AssignmentStatus.Active)
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 && experiment.status === types_1.AssignmentStatus.Active) {
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
- declare type ExperimentDBM = Saved<BaseExperiment> & {
4
+ type ExperimentDBM = Saved<BaseExperiment> & {
5
5
  rules: string | null;
6
6
  };
7
7
  export declare class ExperimentDao extends CommonDao<Experiment, ExperimentDBM> {
@@ -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 declare type BaseExperiment = BaseDBEntity<number> & {
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 declare type Experiment = BaseExperiment & {
14
+ export type Experiment = BaseExperiment & {
13
15
  rules: SegmentationRule[];
14
16
  };
15
- export declare type ExperimentWithBuckets = Saved<Experiment> & {
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 declare type Bucket = BaseDBEntity<number> & {
25
+ export type Bucket = BaseDBEntity<number> & {
24
26
  experimentId: number;
25
27
  key: string;
26
28
  ratio: number;
27
29
  };
28
- export declare type UserAssignment = BaseDBEntity<number> & {
30
+ export type UserAssignment = BaseDBEntity<number> & {
29
31
  userId: string;
30
32
  experimentId: number;
31
33
  bucketId: number | null;
32
34
  };
33
- export declare type GeneratedUserAssignment = Saved<UserAssignment> & {
35
+ export type GeneratedUserAssignment = Saved<UserAssignment> & {
34
36
  bucketKey: string | null;
35
37
  };
36
- export declare type SegmentationData = Record<string, string | boolean | number>;
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
- else {
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.12.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": "^12.19.2",
17
- "@types/node": "^17.0.34",
16
+ "@naturalcycles/dev-lib": "^13.15.0",
17
+ "@types/node": "^18.11.18",
18
18
  "@types/semver": "^7.3.9",
19
- "jest": "^28.1.0"
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": "public"
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": ">=14.15.0"
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 { determineAssignment, validateSegmentationRules, validateTotalBucketRatio } from './util'
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.status !== AssignmentStatus.Active) return null
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.status === AssignmentStatus.Active) {
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)
@@ -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
- Active = 1, // Generating assignments
57
- Paused = 2, // Not generating new assignments, still returning existing
58
- Inactive = 3, // Will not return any assignments
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
  }