@naturalcycles/abba 1.9.1 → 1.11.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 +3 -3
- package/dist/abba.js +31 -14
- package/dist/migrations/init.sql +0 -1
- package/dist/types.d.ts +4 -1
- package/dist/util.d.ts +2 -2
- package/dist/util.js +1 -1
- package/package.json +1 -1
- package/readme.md +2 -2
- package/src/abba.ts +40 -17
- package/src/migrations/init.sql +0 -1
- package/src/types.ts +5 -1
- package/src/util.ts +3 -3
package/dist/abba.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Saved } from '@naturalcycles/js-lib';
|
|
2
|
-
import { AbbaConfig, Bucket, BucketInput, Experiment, ExperimentWithBuckets, UserAssignment } from './types';
|
|
2
|
+
import { AbbaConfig, Bucket, BucketInput, Experiment, ExperimentWithBuckets, GeneratedUserAssignment, UserAssignment } from './types';
|
|
3
3
|
import { SegmentationData, AssignmentStatistics } from '.';
|
|
4
4
|
export declare class Abba {
|
|
5
5
|
cfg: AbbaConfig;
|
|
@@ -45,7 +45,7 @@ export declare class Abba {
|
|
|
45
45
|
* @param existingOnly Do not generate any new assignments for this experiment
|
|
46
46
|
* @param segmentationData Required if existingOnly is false
|
|
47
47
|
*/
|
|
48
|
-
getUserAssignment(experimentId: number, userId: string, existingOnly: boolean, segmentationData?: SegmentationData): Promise<
|
|
48
|
+
getUserAssignment(experimentId: number, userId: string, existingOnly: boolean, segmentationData?: SegmentationData): Promise<GeneratedUserAssignment | null>;
|
|
49
49
|
/**
|
|
50
50
|
* Get all existing user assignments.
|
|
51
51
|
* Hot method.
|
|
@@ -57,7 +57,7 @@ export declare class Abba {
|
|
|
57
57
|
* Will return any existing and attempt to generate any new assignments.
|
|
58
58
|
* Hot method.
|
|
59
59
|
*/
|
|
60
|
-
generateUserAssignments(userId: string, segmentationData: SegmentationData): Promise<
|
|
60
|
+
generateUserAssignments(userId: string, segmentationData: SegmentationData): Promise<GeneratedUserAssignment[]>;
|
|
61
61
|
/**
|
|
62
62
|
* Get assignment statistics for an experiment.
|
|
63
63
|
* Cold method.
|
package/dist/abba.js
CHANGED
|
@@ -99,8 +99,14 @@ class Abba {
|
|
|
99
99
|
*/
|
|
100
100
|
async getUserAssignment(experimentId, userId, existingOnly, segmentationData) {
|
|
101
101
|
const existing = await this.getExistingUserAssignment(experimentId, userId);
|
|
102
|
-
if (existing)
|
|
103
|
-
|
|
102
|
+
if (existing) {
|
|
103
|
+
let bucketKey = null;
|
|
104
|
+
if (existing.bucketId) {
|
|
105
|
+
const { key } = await this.bucketDao.requireById(existing.bucketId);
|
|
106
|
+
bucketKey = key;
|
|
107
|
+
}
|
|
108
|
+
return { ...existing, bucketKey };
|
|
109
|
+
}
|
|
104
110
|
if (existingOnly)
|
|
105
111
|
return null;
|
|
106
112
|
const experiment = await this.experimentDao.requireById(experimentId);
|
|
@@ -111,7 +117,11 @@ class Abba {
|
|
|
111
117
|
const assignment = this.generateUserAssignmentData({ ...experiment, buckets }, userId, segmentationData);
|
|
112
118
|
if (!assignment)
|
|
113
119
|
return null;
|
|
114
|
-
|
|
120
|
+
const saved = await this.userAssignmentDao.save(assignment);
|
|
121
|
+
return {
|
|
122
|
+
...saved,
|
|
123
|
+
bucketKey: buckets.find(b => b.id === saved.bucketId)?.key || null,
|
|
124
|
+
};
|
|
115
125
|
}
|
|
116
126
|
/**
|
|
117
127
|
* Get all existing user assignments.
|
|
@@ -129,22 +139,28 @@ class Abba {
|
|
|
129
139
|
async generateUserAssignments(userId, segmentationData) {
|
|
130
140
|
const experiments = await this.getActiveExperiments(); // cached
|
|
131
141
|
const existingAssignments = await this.getAllExistingUserAssignments(userId);
|
|
132
|
-
const
|
|
133
|
-
const generatedAssignments = [];
|
|
142
|
+
const newAssignments = [];
|
|
134
143
|
for (const experiment of experiments) {
|
|
135
144
|
const existing = existingAssignments.find(ua => ua.experimentId === experiment.id);
|
|
136
|
-
if (existing) {
|
|
137
|
-
allAssignments.push(existing);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
145
|
+
if (!existing) {
|
|
140
146
|
const assignment = this.generateUserAssignmentData(experiment, userId, segmentationData);
|
|
141
147
|
if (assignment) {
|
|
142
|
-
|
|
148
|
+
newAssignments.push(assignment);
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
}
|
|
146
|
-
await this.userAssignmentDao.saveBatch(
|
|
147
|
-
|
|
152
|
+
existingAssignments.push(...(await this.userAssignmentDao.saveBatch(newAssignments)));
|
|
153
|
+
const assignments = [];
|
|
154
|
+
for (const experiment of experiments) {
|
|
155
|
+
const existing = existingAssignments.find(ua => ua.experimentId === experiment.id);
|
|
156
|
+
if (existing) {
|
|
157
|
+
assignments.push({
|
|
158
|
+
...existing,
|
|
159
|
+
bucketKey: experiment.buckets.find(i => i.id === existing.bucketId)?.key || null,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return assignments;
|
|
148
164
|
}
|
|
149
165
|
/**
|
|
150
166
|
* Get assignment statistics for an experiment.
|
|
@@ -160,7 +176,7 @@ class Abba {
|
|
|
160
176
|
};
|
|
161
177
|
const buckets = await this.bucketDao.getBy('experimentId', experimentId);
|
|
162
178
|
await (0, js_lib_1.pMap)(buckets, async (bucket) => {
|
|
163
|
-
statistics[bucket.id] = await this.userAssignmentDao
|
|
179
|
+
statistics.buckets[bucket.id] = await this.userAssignmentDao
|
|
164
180
|
.query()
|
|
165
181
|
.filterEq('bucketId', bucket.id)
|
|
166
182
|
.runQueryCount();
|
|
@@ -175,10 +191,11 @@ class Abba {
|
|
|
175
191
|
const segmentationMatch = (0, util_1.validateSegmentationRules)(experiment.rules, segmentationData);
|
|
176
192
|
if (!segmentationMatch)
|
|
177
193
|
return null;
|
|
194
|
+
const bucket = (0, util_1.determineAssignment)(experiment.sampling, experiment.buckets);
|
|
178
195
|
return {
|
|
179
196
|
userId,
|
|
180
197
|
experimentId: experiment.id,
|
|
181
|
-
bucketId:
|
|
198
|
+
bucketId: bucket?.id || null,
|
|
182
199
|
};
|
|
183
200
|
}
|
|
184
201
|
/**
|
package/dist/migrations/init.sql
CHANGED
|
@@ -13,7 +13,6 @@ CREATE TABLE IF NOT EXISTS `Bucket` (
|
|
|
13
13
|
-- CreateTable
|
|
14
14
|
CREATE TABLE IF NOT EXISTS `Experiment` (
|
|
15
15
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
|
16
|
-
`name` VARCHAR(191) NOT NULL,
|
|
17
16
|
`status` INTEGER NOT NULL,
|
|
18
17
|
`sampling` INTEGER NOT NULL,
|
|
19
18
|
`description` VARCHAR(240) NULL,
|
package/dist/types.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface AbbaConfig {
|
|
|
4
4
|
db: CommonDB;
|
|
5
5
|
}
|
|
6
6
|
export declare type BaseExperiment = BaseDBEntity<number> & {
|
|
7
|
-
|
|
7
|
+
id: number;
|
|
8
8
|
status: number;
|
|
9
9
|
sampling: number;
|
|
10
10
|
description: string | null;
|
|
@@ -30,6 +30,9 @@ export declare type UserAssignment = BaseDBEntity<number> & {
|
|
|
30
30
|
experimentId: number;
|
|
31
31
|
bucketId: number | null;
|
|
32
32
|
};
|
|
33
|
+
export declare type GeneratedUserAssignment = Saved<UserAssignment> & {
|
|
34
|
+
bucketKey: string | null;
|
|
35
|
+
};
|
|
33
36
|
export declare type SegmentationData = Record<string, string | boolean | number>;
|
|
34
37
|
export declare enum AssignmentStatus {
|
|
35
38
|
Active = 1,
|
package/dist/util.d.ts
CHANGED
|
@@ -7,11 +7,11 @@ export declare const rollDie: () => number;
|
|
|
7
7
|
/**
|
|
8
8
|
* Determines a users assignment for this experiment. Returns null if they are not considered to be in the sampling group
|
|
9
9
|
*/
|
|
10
|
-
export declare const determineAssignment: (sampling: number, buckets: Saved<Bucket>[]) =>
|
|
10
|
+
export declare const determineAssignment: (sampling: number, buckets: Saved<Bucket>[]) => Bucket | null;
|
|
11
11
|
/**
|
|
12
12
|
* Determines which bucket a user assignment will recieve
|
|
13
13
|
*/
|
|
14
|
-
export declare const determineBucket: (buckets: Saved<Bucket>[]) =>
|
|
14
|
+
export declare const determineBucket: (buckets: Saved<Bucket>[]) => Bucket;
|
|
15
15
|
/**
|
|
16
16
|
* Validate the total ratio of the buckets equals 100
|
|
17
17
|
*/
|
package/dist/util.js
CHANGED
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -154,7 +154,7 @@ async getUserAssignment(
|
|
|
154
154
|
userId: string,
|
|
155
155
|
existingOnly: boolean,
|
|
156
156
|
segmentationData?: SegmentationData,
|
|
157
|
-
): Promise<
|
|
157
|
+
): Promise<GeneratedUserAssignment | null>
|
|
158
158
|
```
|
|
159
159
|
|
|
160
160
|
### Generate user assignments
|
|
@@ -166,7 +166,7 @@ attempt to generate new assignments.
|
|
|
166
166
|
async generateUserAssignments(
|
|
167
167
|
userId: string,
|
|
168
168
|
segmentationData: SegmentationData,
|
|
169
|
-
): Promise<
|
|
169
|
+
): Promise<GeneratedUserAssignment[]>
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
### Getting assignment statistics
|
package/src/abba.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
BucketInput,
|
|
8
8
|
Experiment,
|
|
9
9
|
ExperimentWithBuckets,
|
|
10
|
+
GeneratedUserAssignment,
|
|
10
11
|
UserAssignment,
|
|
11
12
|
} from './types'
|
|
12
13
|
import { determineAssignment, validateSegmentationRules, validateTotalBucketRatio } from './util'
|
|
@@ -96,7 +97,7 @@ export class Abba {
|
|
|
96
97
|
return {
|
|
97
98
|
...(experiment as Saved<Experiment>),
|
|
98
99
|
buckets: await this.bucketDao.saveBatch(
|
|
99
|
-
buckets.map(b => ({ ...b, experimentId: experiment.id
|
|
100
|
+
buckets.map(b => ({ ...b, experimentId: experiment.id })),
|
|
100
101
|
),
|
|
101
102
|
}
|
|
102
103
|
}
|
|
@@ -145,9 +146,18 @@ export class Abba {
|
|
|
145
146
|
userId: string,
|
|
146
147
|
existingOnly: boolean,
|
|
147
148
|
segmentationData?: SegmentationData,
|
|
148
|
-
): Promise<
|
|
149
|
+
): Promise<GeneratedUserAssignment | null> {
|
|
149
150
|
const existing = await this.getExistingUserAssignment(experimentId, userId)
|
|
150
|
-
|
|
151
|
+
|
|
152
|
+
if (existing) {
|
|
153
|
+
let bucketKey = null
|
|
154
|
+
if (existing.bucketId) {
|
|
155
|
+
const { key } = await this.bucketDao.requireById(existing.bucketId)
|
|
156
|
+
bucketKey = key
|
|
157
|
+
}
|
|
158
|
+
return { ...existing, bucketKey }
|
|
159
|
+
}
|
|
160
|
+
|
|
151
161
|
if (existingOnly) return null
|
|
152
162
|
|
|
153
163
|
const experiment = await this.experimentDao.requireById(experimentId)
|
|
@@ -164,7 +174,12 @@ export class Abba {
|
|
|
164
174
|
)
|
|
165
175
|
if (!assignment) return null
|
|
166
176
|
|
|
167
|
-
|
|
177
|
+
const saved = await this.userAssignmentDao.save(assignment)
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
...saved,
|
|
181
|
+
bucketKey: buckets.find(b => b.id === saved.bucketId)?.key || null,
|
|
182
|
+
}
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
/**
|
|
@@ -184,28 +199,34 @@ export class Abba {
|
|
|
184
199
|
async generateUserAssignments(
|
|
185
200
|
userId: string,
|
|
186
201
|
segmentationData: SegmentationData,
|
|
187
|
-
): Promise<
|
|
202
|
+
): Promise<GeneratedUserAssignment[]> {
|
|
188
203
|
const experiments = await this.getActiveExperiments() // cached
|
|
189
204
|
const existingAssignments = await this.getAllExistingUserAssignments(userId)
|
|
190
205
|
|
|
191
|
-
const
|
|
192
|
-
const generatedAssignments: UserAssignment[] = []
|
|
193
|
-
|
|
206
|
+
const newAssignments: UserAssignment[] = []
|
|
194
207
|
for (const experiment of experiments) {
|
|
195
208
|
const existing = existingAssignments.find(ua => ua.experimentId === experiment.id)
|
|
196
|
-
if (existing) {
|
|
197
|
-
allAssignments.push(existing)
|
|
198
|
-
} else {
|
|
209
|
+
if (!existing) {
|
|
199
210
|
const assignment = this.generateUserAssignmentData(experiment, userId, segmentationData)
|
|
200
211
|
if (assignment) {
|
|
201
|
-
|
|
212
|
+
newAssignments.push(assignment)
|
|
202
213
|
}
|
|
203
214
|
}
|
|
204
215
|
}
|
|
205
216
|
|
|
206
|
-
await this.userAssignmentDao.saveBatch(
|
|
217
|
+
existingAssignments.push(...(await this.userAssignmentDao.saveBatch(newAssignments)))
|
|
207
218
|
|
|
208
|
-
|
|
219
|
+
const assignments: GeneratedUserAssignment[] = []
|
|
220
|
+
for (const experiment of experiments) {
|
|
221
|
+
const existing = existingAssignments.find(ua => ua.experimentId === experiment.id)
|
|
222
|
+
if (existing) {
|
|
223
|
+
assignments.push({
|
|
224
|
+
...existing,
|
|
225
|
+
bucketKey: experiment.buckets.find(i => i.id === existing.bucketId)?.key || null,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return assignments
|
|
209
230
|
}
|
|
210
231
|
|
|
211
232
|
/**
|
|
@@ -213,7 +234,7 @@ export class Abba {
|
|
|
213
234
|
* Cold method.
|
|
214
235
|
*/
|
|
215
236
|
async getExperimentAssignmentStatistics(experimentId: number): Promise<AssignmentStatistics> {
|
|
216
|
-
const statistics = {
|
|
237
|
+
const statistics: AssignmentStatistics = {
|
|
217
238
|
sampled: await this.userAssignmentDao
|
|
218
239
|
.query()
|
|
219
240
|
.filterEq('experimentId', experimentId)
|
|
@@ -223,7 +244,7 @@ export class Abba {
|
|
|
223
244
|
|
|
224
245
|
const buckets = await this.bucketDao.getBy('experimentId', experimentId)
|
|
225
246
|
await pMap(buckets, async bucket => {
|
|
226
|
-
statistics[bucket.id] = await this.userAssignmentDao
|
|
247
|
+
statistics.buckets[bucket.id] = await this.userAssignmentDao
|
|
227
248
|
.query()
|
|
228
249
|
.filterEq('bucketId', bucket.id)
|
|
229
250
|
.runQueryCount()
|
|
@@ -244,10 +265,12 @@ export class Abba {
|
|
|
244
265
|
const segmentationMatch = validateSegmentationRules(experiment.rules, segmentationData)
|
|
245
266
|
if (!segmentationMatch) return null
|
|
246
267
|
|
|
268
|
+
const bucket = determineAssignment(experiment.sampling, experiment.buckets)
|
|
269
|
+
|
|
247
270
|
return {
|
|
248
271
|
userId,
|
|
249
272
|
experimentId: experiment.id,
|
|
250
|
-
bucketId:
|
|
273
|
+
bucketId: bucket?.id || null,
|
|
251
274
|
}
|
|
252
275
|
}
|
|
253
276
|
|
package/src/migrations/init.sql
CHANGED
|
@@ -13,7 +13,6 @@ CREATE TABLE IF NOT EXISTS `Bucket` (
|
|
|
13
13
|
-- CreateTable
|
|
14
14
|
CREATE TABLE IF NOT EXISTS `Experiment` (
|
|
15
15
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
|
16
|
-
`name` VARCHAR(191) NOT NULL,
|
|
17
16
|
`status` INTEGER NOT NULL,
|
|
18
17
|
`sampling` INTEGER NOT NULL,
|
|
19
18
|
`description` VARCHAR(240) NULL,
|
package/src/types.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface AbbaConfig {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export type BaseExperiment = BaseDBEntity<number> & {
|
|
17
|
-
|
|
17
|
+
id: number
|
|
18
18
|
status: number
|
|
19
19
|
sampling: number
|
|
20
20
|
description: string | null
|
|
@@ -46,6 +46,10 @@ export type UserAssignment = BaseDBEntity<number> & {
|
|
|
46
46
|
bucketId: number | null
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export type GeneratedUserAssignment = Saved<UserAssignment> & {
|
|
50
|
+
bucketKey: string | null
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
export type SegmentationData = Record<string, string | boolean | number>
|
|
50
54
|
|
|
51
55
|
export enum AssignmentStatus {
|
package/src/util.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const rollDie = (): number => {
|
|
|
12
12
|
/**
|
|
13
13
|
* Determines a users assignment for this experiment. Returns null if they are not considered to be in the sampling group
|
|
14
14
|
*/
|
|
15
|
-
export const determineAssignment = (sampling: number, buckets: Saved<Bucket>[]):
|
|
15
|
+
export const determineAssignment = (sampling: number, buckets: Saved<Bucket>[]): Bucket | null => {
|
|
16
16
|
// Should this person be considered for the experiment?
|
|
17
17
|
if (rollDie() > sampling) {
|
|
18
18
|
return null
|
|
@@ -25,7 +25,7 @@ export const determineAssignment = (sampling: number, buckets: Saved<Bucket>[]):
|
|
|
25
25
|
/**
|
|
26
26
|
* Determines which bucket a user assignment will recieve
|
|
27
27
|
*/
|
|
28
|
-
export const determineBucket = (buckets: Saved<Bucket>[]):
|
|
28
|
+
export const determineBucket = (buckets: Saved<Bucket>[]): Bucket => {
|
|
29
29
|
const bucketRoll = rollDie()
|
|
30
30
|
let range: [number, number] | undefined
|
|
31
31
|
const bucket = buckets.find(b => {
|
|
@@ -44,7 +44,7 @@ export const determineBucket = (buckets: Saved<Bucket>[]): number => {
|
|
|
44
44
|
throw new Error('Could not detetermine bucket from ratios')
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
return bucket
|
|
47
|
+
return bucket
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|