@naturalcycles/abba 2.8.0 → 2.10.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.d.ts +1 -0
- package/dist/abba.js +18 -4
- package/dist/types.d.ts +7 -0
- package/dist/types.js +7 -0
- package/dist/util.js +6 -7
- package/package.json +3 -3
- package/src/abba.ts +19 -3
- package/src/types.ts +8 -0
- package/src/util.ts +10 -7
package/dist/abba.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare class Abba {
|
|
|
41
41
|
* Cold method.
|
|
42
42
|
*/
|
|
43
43
|
deleteExperiment(experimentId: string): Promise<void>;
|
|
44
|
+
getExperimentWithBuckets(experimentId: string): Promise<ExperimentWithBuckets | undefined>;
|
|
44
45
|
/**
|
|
45
46
|
* Get an assignment for a given user. If existingOnly is false, it will attempt to generate a new assignment
|
|
46
47
|
* Cold method.
|
package/dist/abba.js
CHANGED
|
@@ -8,7 +8,7 @@ import { LRUMemoCache } from '@naturalcycles/nodejs-lib/lruMemoCache';
|
|
|
8
8
|
import { bucketDao } from './dao/bucket.dao.js';
|
|
9
9
|
import { experimentDao } from './dao/experiment.dao.js';
|
|
10
10
|
import { userAssignmentDao } from './dao/userAssignment.dao.js';
|
|
11
|
-
import { AssignmentStatus } from './types.js';
|
|
11
|
+
import { AbbaErrorCode, AssignmentStatus } from './types.js';
|
|
12
12
|
import { canGenerateNewAssignments, generateUserAssignmentData, getUserExclusionSet, validateTotalBucketRatio, } from './util.js';
|
|
13
13
|
/**
|
|
14
14
|
* 10 minutes
|
|
@@ -150,12 +150,22 @@ export class Abba {
|
|
|
150
150
|
const experiment = await this.experimentDao.requireById(experimentId);
|
|
151
151
|
const hasBeenInactiveFor15Mins = experiment.status === AssignmentStatus.Inactive &&
|
|
152
152
|
localTime(experiment.updated).isOlderThan(15, 'minute');
|
|
153
|
-
_assert(hasBeenInactiveFor15Mins, 'Experiment must be inactive for at least 15 minutes before deletion');
|
|
153
|
+
_assert(hasBeenInactiveFor15Mins, 'Experiment must be inactive for at least 15 minutes before deletion', { code: AbbaErrorCode.ExperimentDeletionTooSoon });
|
|
154
154
|
await this.userAssignmentDao.deleteByExperimentId(experimentId);
|
|
155
155
|
await this.bucketDao.deleteByExperimentId(experimentId);
|
|
156
156
|
await this.experimentDao.deleteById(experimentId);
|
|
157
157
|
await this.updateExclusions(experimentId, []);
|
|
158
158
|
}
|
|
159
|
+
async getExperimentWithBuckets(experimentId) {
|
|
160
|
+
const experiment = await this.experimentDao.getById(experimentId);
|
|
161
|
+
if (!experiment)
|
|
162
|
+
return;
|
|
163
|
+
const buckets = await this.bucketDao.getByExperimentId(experiment.id);
|
|
164
|
+
return {
|
|
165
|
+
...experiment,
|
|
166
|
+
buckets,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
159
169
|
/**
|
|
160
170
|
* Get an assignment for a given user. If existingOnly is false, it will attempt to generate a new assignment
|
|
161
171
|
* Cold method.
|
|
@@ -167,7 +177,9 @@ export class Abba {
|
|
|
167
177
|
*/
|
|
168
178
|
async getUserAssignment(experimentKey, userId, existingOnly, segmentationData) {
|
|
169
179
|
const experiment = await this.experimentDao.getByKey(experimentKey);
|
|
170
|
-
_assert(experiment, `Experiment does not exist: ${experimentKey}
|
|
180
|
+
_assert(experiment, `Experiment does not exist: ${experimentKey}`, {
|
|
181
|
+
code: AbbaErrorCode.ExperimentNotFound,
|
|
182
|
+
});
|
|
171
183
|
// Inactive experiments should never return an assignment
|
|
172
184
|
if (experiment.status === AssignmentStatus.Inactive) {
|
|
173
185
|
return null;
|
|
@@ -193,7 +205,9 @@ export class Abba {
|
|
|
193
205
|
if (!canGenerateNewAssignments(experiment, exclusionSet)) {
|
|
194
206
|
return null;
|
|
195
207
|
}
|
|
196
|
-
_assert(segmentationData, 'Segmentation data required when creating a new assignment'
|
|
208
|
+
_assert(segmentationData, 'Segmentation data required when creating a new assignment', {
|
|
209
|
+
code: AbbaErrorCode.SegmentationDataRequired,
|
|
210
|
+
});
|
|
197
211
|
const experimentWithBuckets = { ...experiment, buckets };
|
|
198
212
|
const assignment = generateUserAssignmentData(experimentWithBuckets, userId, segmentationData);
|
|
199
213
|
if (!assignment) {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { CommonDB } from '@naturalcycles/db-lib';
|
|
2
2
|
import type { AnyObject, BaseDBEntity, IsoDate } from '@naturalcycles/js-lib/types';
|
|
3
|
+
export declare const AbbaErrorCode: {
|
|
4
|
+
readonly ExperimentNotFound: 'abba/experimentNotFound';
|
|
5
|
+
readonly ExperimentDeletionTooSoon: 'abba/experimentDeletionTooSoon';
|
|
6
|
+
readonly SegmentationDataRequired: 'abba/segmentationDataRequired';
|
|
7
|
+
readonly InvalidBucketRatio: 'abba/invalidBucketRatio';
|
|
8
|
+
readonly BucketDeterminationFailed: 'abba/bucketDeterminationFailed';
|
|
9
|
+
};
|
|
3
10
|
export interface AbbaConfig {
|
|
4
11
|
db: CommonDB;
|
|
5
12
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export const AbbaErrorCode = {
|
|
2
|
+
ExperimentNotFound: 'abba/experimentNotFound',
|
|
3
|
+
ExperimentDeletionTooSoon: 'abba/experimentDeletionTooSoon',
|
|
4
|
+
SegmentationDataRequired: 'abba/segmentationDataRequired',
|
|
5
|
+
InvalidBucketRatio: 'abba/invalidBucketRatio',
|
|
6
|
+
BucketDeterminationFailed: 'abba/bucketDeterminationFailed',
|
|
7
|
+
};
|
|
1
8
|
export var AssignmentStatus;
|
|
2
9
|
(function (AssignmentStatus) {
|
|
3
10
|
/**
|
package/dist/util.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { localDate } from '@naturalcycles/js-lib/datetime';
|
|
2
|
+
import { _assert } from '@naturalcycles/js-lib/error/assert.js';
|
|
2
3
|
import { satisfies } from 'semver';
|
|
3
|
-
import { AssignmentStatus, SegmentationRuleOperator } from './types.js';
|
|
4
|
+
import { AbbaErrorCode, AssignmentStatus, SegmentationRuleOperator } from './types.js';
|
|
4
5
|
/**
|
|
5
6
|
* Generate a new assignment for a given user.
|
|
6
7
|
* Doesn't save it.
|
|
@@ -53,9 +54,9 @@ export function determineBucket(buckets) {
|
|
|
53
54
|
return b;
|
|
54
55
|
}
|
|
55
56
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
57
|
+
_assert(bucket, 'Could not determine bucket from ratios', {
|
|
58
|
+
code: AbbaErrorCode.BucketDeterminationFailed,
|
|
59
|
+
});
|
|
59
60
|
return bucket;
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
@@ -63,9 +64,7 @@ export function determineBucket(buckets) {
|
|
|
63
64
|
*/
|
|
64
65
|
export function validateTotalBucketRatio(buckets) {
|
|
65
66
|
const bucketSum = buckets.reduce((sum, current) => sum + current.ratio, 0);
|
|
66
|
-
|
|
67
|
-
throw new Error('Total bucket ratio must be 100 before you can activate an experiment');
|
|
68
|
-
}
|
|
67
|
+
_assert(bucketSum === 100, 'Total bucket ratio must be 100 before you can activate an experiment', { code: AbbaErrorCode.InvalidBucketRatio });
|
|
69
68
|
}
|
|
70
69
|
/**
|
|
71
70
|
* Validate a users segmentation data against multiple rules. Returns false if any fail
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/abba",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.10.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/db-lib": "^10",
|
|
7
7
|
"@naturalcycles/js-lib": "^15",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@types/semver": "^7",
|
|
14
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
15
|
-
"@naturalcycles/dev-lib": "
|
|
14
|
+
"@typescript/native-preview": "7.0.0-dev.20260415.1",
|
|
15
|
+
"@naturalcycles/dev-lib": "18.4.2"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./dist/index.js"
|
package/src/abba.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
UserAssignment,
|
|
24
24
|
UserExperiment,
|
|
25
25
|
} from './types.js'
|
|
26
|
-
import { AssignmentStatus } from './types.js'
|
|
26
|
+
import { AbbaErrorCode, AssignmentStatus } from './types.js'
|
|
27
27
|
import {
|
|
28
28
|
canGenerateNewAssignments,
|
|
29
29
|
generateUserAssignmentData,
|
|
@@ -233,6 +233,7 @@ export class Abba {
|
|
|
233
233
|
_assert(
|
|
234
234
|
hasBeenInactiveFor15Mins,
|
|
235
235
|
'Experiment must be inactive for at least 15 minutes before deletion',
|
|
236
|
+
{ code: AbbaErrorCode.ExperimentDeletionTooSoon },
|
|
236
237
|
)
|
|
237
238
|
|
|
238
239
|
await this.userAssignmentDao.deleteByExperimentId(experimentId)
|
|
@@ -241,6 +242,17 @@ export class Abba {
|
|
|
241
242
|
await this.updateExclusions(experimentId, [])
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
async getExperimentWithBuckets(experimentId: string): Promise<ExperimentWithBuckets | undefined> {
|
|
246
|
+
const experiment = await this.experimentDao.getById(experimentId)
|
|
247
|
+
if (!experiment) return
|
|
248
|
+
|
|
249
|
+
const buckets = await this.bucketDao.getByExperimentId(experiment.id)
|
|
250
|
+
return {
|
|
251
|
+
...experiment,
|
|
252
|
+
buckets,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
244
256
|
/**
|
|
245
257
|
* Get an assignment for a given user. If existingOnly is false, it will attempt to generate a new assignment
|
|
246
258
|
* Cold method.
|
|
@@ -257,7 +269,9 @@ export class Abba {
|
|
|
257
269
|
segmentationData?: SegmentationData,
|
|
258
270
|
): Promise<DecoratedUserAssignment | null> {
|
|
259
271
|
const experiment = await this.experimentDao.getByKey(experimentKey)
|
|
260
|
-
_assert(experiment, `Experiment does not exist: ${experimentKey}
|
|
272
|
+
_assert(experiment, `Experiment does not exist: ${experimentKey}`, {
|
|
273
|
+
code: AbbaErrorCode.ExperimentNotFound,
|
|
274
|
+
})
|
|
261
275
|
|
|
262
276
|
// Inactive experiments should never return an assignment
|
|
263
277
|
if (experiment.status === AssignmentStatus.Inactive) {
|
|
@@ -291,7 +305,9 @@ export class Abba {
|
|
|
291
305
|
return null
|
|
292
306
|
}
|
|
293
307
|
|
|
294
|
-
_assert(segmentationData, 'Segmentation data required when creating a new assignment'
|
|
308
|
+
_assert(segmentationData, 'Segmentation data required when creating a new assignment', {
|
|
309
|
+
code: AbbaErrorCode.SegmentationDataRequired,
|
|
310
|
+
})
|
|
295
311
|
|
|
296
312
|
const experimentWithBuckets = { ...experiment, buckets }
|
|
297
313
|
const assignment = generateUserAssignmentData(experimentWithBuckets, userId, segmentationData)
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { CommonDB } from '@naturalcycles/db-lib'
|
|
2
2
|
import type { AnyObject, BaseDBEntity, IsoDate } from '@naturalcycles/js-lib/types'
|
|
3
3
|
|
|
4
|
+
export const AbbaErrorCode = {
|
|
5
|
+
ExperimentNotFound: 'abba/experimentNotFound',
|
|
6
|
+
ExperimentDeletionTooSoon: 'abba/experimentDeletionTooSoon',
|
|
7
|
+
SegmentationDataRequired: 'abba/segmentationDataRequired',
|
|
8
|
+
InvalidBucketRatio: 'abba/invalidBucketRatio',
|
|
9
|
+
BucketDeterminationFailed: 'abba/bucketDeterminationFailed',
|
|
10
|
+
} as const
|
|
11
|
+
|
|
4
12
|
export interface AbbaConfig {
|
|
5
13
|
db: CommonDB
|
|
6
14
|
}
|
package/src/util.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { localDate } from '@naturalcycles/js-lib/datetime'
|
|
2
|
+
import { _assert } from '@naturalcycles/js-lib/error/assert.js'
|
|
2
3
|
import type { Unsaved } from '@naturalcycles/js-lib/types'
|
|
3
4
|
import { satisfies } from 'semver'
|
|
4
5
|
import type {
|
|
@@ -13,7 +14,7 @@ import type {
|
|
|
13
14
|
UserAssignment,
|
|
14
15
|
UserExperiment,
|
|
15
16
|
} from './types.js'
|
|
16
|
-
import { AssignmentStatus, SegmentationRuleOperator } from './types.js'
|
|
17
|
+
import { AbbaErrorCode, AssignmentStatus, SegmentationRuleOperator } from './types.js'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Generate a new assignment for a given user.
|
|
@@ -78,9 +79,9 @@ export function determineBucket(buckets: Bucket[]): Bucket {
|
|
|
78
79
|
}
|
|
79
80
|
})
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
82
|
+
_assert(bucket, 'Could not determine bucket from ratios', {
|
|
83
|
+
code: AbbaErrorCode.BucketDeterminationFailed,
|
|
84
|
+
})
|
|
84
85
|
|
|
85
86
|
return bucket
|
|
86
87
|
}
|
|
@@ -90,9 +91,11 @@ export function determineBucket(buckets: Bucket[]): Bucket {
|
|
|
90
91
|
*/
|
|
91
92
|
export function validateTotalBucketRatio(buckets: (Unsaved<Bucket> | BucketInput)[]): void {
|
|
92
93
|
const bucketSum = buckets.reduce((sum, current) => sum + current.ratio, 0)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
_assert(
|
|
95
|
+
bucketSum === 100,
|
|
96
|
+
'Total bucket ratio must be 100 before you can activate an experiment',
|
|
97
|
+
{ code: AbbaErrorCode.InvalidBucketRatio },
|
|
98
|
+
)
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
/**
|