@naturalcycles/abba 1.9.2 → 1.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 +3 -3
- package/dist/abba.js +33 -13
- package/dist/types.d.ts +4 -0
- 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 -14
- package/src/types.ts +5 -0
- 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,15 @@ 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
|
+
const experiment = await this.experimentDao.requireById(experimentId);
|
|
104
|
+
const bucket = await this.bucketDao.getById(existing.bucketId || undefined);
|
|
105
|
+
return {
|
|
106
|
+
...existing,
|
|
107
|
+
experimentName: experiment.name,
|
|
108
|
+
bucketKey: bucket?.key || null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
104
111
|
if (existingOnly)
|
|
105
112
|
return null;
|
|
106
113
|
const experiment = await this.experimentDao.requireById(experimentId);
|
|
@@ -111,7 +118,12 @@ class Abba {
|
|
|
111
118
|
const assignment = this.generateUserAssignmentData({ ...experiment, buckets }, userId, segmentationData);
|
|
112
119
|
if (!assignment)
|
|
113
120
|
return null;
|
|
114
|
-
|
|
121
|
+
const saved = await this.userAssignmentDao.save(assignment);
|
|
122
|
+
return {
|
|
123
|
+
...saved,
|
|
124
|
+
experimentName: experiment.name,
|
|
125
|
+
bucketKey: buckets.find(b => b.id === saved.bucketId)?.key || null,
|
|
126
|
+
};
|
|
115
127
|
}
|
|
116
128
|
/**
|
|
117
129
|
* Get all existing user assignments.
|
|
@@ -129,22 +141,29 @@ class Abba {
|
|
|
129
141
|
async generateUserAssignments(userId, segmentationData) {
|
|
130
142
|
const experiments = await this.getActiveExperiments(); // cached
|
|
131
143
|
const existingAssignments = await this.getAllExistingUserAssignments(userId);
|
|
132
|
-
const
|
|
133
|
-
const generatedAssignments = [];
|
|
144
|
+
const newAssignments = [];
|
|
134
145
|
for (const experiment of experiments) {
|
|
135
146
|
const existing = existingAssignments.find(ua => ua.experimentId === experiment.id);
|
|
136
|
-
if (existing) {
|
|
137
|
-
allAssignments.push(existing);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
147
|
+
if (!existing) {
|
|
140
148
|
const assignment = this.generateUserAssignmentData(experiment, userId, segmentationData);
|
|
141
149
|
if (assignment) {
|
|
142
|
-
|
|
150
|
+
newAssignments.push(assignment);
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
|
-
await this.userAssignmentDao.saveBatch(
|
|
147
|
-
|
|
154
|
+
existingAssignments.push(...(await this.userAssignmentDao.saveBatch(newAssignments)));
|
|
155
|
+
const assignments = [];
|
|
156
|
+
for (const experiment of experiments) {
|
|
157
|
+
const existing = existingAssignments.find(ua => ua.experimentId === experiment.id);
|
|
158
|
+
if (existing) {
|
|
159
|
+
assignments.push({
|
|
160
|
+
...existing,
|
|
161
|
+
experimentName: experiment.name,
|
|
162
|
+
bucketKey: experiment.buckets.find(i => i.id === existing.bucketId)?.key || null,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return assignments;
|
|
148
167
|
}
|
|
149
168
|
/**
|
|
150
169
|
* Get assignment statistics for an experiment.
|
|
@@ -175,10 +194,11 @@ class Abba {
|
|
|
175
194
|
const segmentationMatch = (0, util_1.validateSegmentationRules)(experiment.rules, segmentationData);
|
|
176
195
|
if (!segmentationMatch)
|
|
177
196
|
return null;
|
|
197
|
+
const bucket = (0, util_1.determineAssignment)(experiment.sampling, experiment.buckets);
|
|
178
198
|
return {
|
|
179
199
|
userId,
|
|
180
200
|
experimentId: experiment.id,
|
|
181
|
-
bucketId:
|
|
201
|
+
bucketId: bucket?.id || null,
|
|
182
202
|
};
|
|
183
203
|
}
|
|
184
204
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -30,6 +30,10 @@ 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
|
+
experimentName: string;
|
|
35
|
+
bucketKey: string | null;
|
|
36
|
+
};
|
|
33
37
|
export declare type SegmentationData = Record<string, string | boolean | number>;
|
|
34
38
|
export declare enum AssignmentStatus {
|
|
35
39
|
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'
|
|
@@ -145,9 +146,19 @@ 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
|
+
const experiment = await this.experimentDao.requireById(experimentId)
|
|
154
|
+
const bucket = await this.bucketDao.getById(existing.bucketId || undefined)
|
|
155
|
+
return {
|
|
156
|
+
...existing,
|
|
157
|
+
experimentName: experiment.name,
|
|
158
|
+
bucketKey: bucket?.key || null,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
if (existingOnly) return null
|
|
152
163
|
|
|
153
164
|
const experiment = await this.experimentDao.requireById(experimentId)
|
|
@@ -164,7 +175,13 @@ export class Abba {
|
|
|
164
175
|
)
|
|
165
176
|
if (!assignment) return null
|
|
166
177
|
|
|
167
|
-
|
|
178
|
+
const saved = await this.userAssignmentDao.save(assignment)
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
...saved,
|
|
182
|
+
experimentName: experiment.name,
|
|
183
|
+
bucketKey: buckets.find(b => b.id === saved.bucketId)?.key || null,
|
|
184
|
+
}
|
|
168
185
|
}
|
|
169
186
|
|
|
170
187
|
/**
|
|
@@ -184,28 +201,35 @@ export class Abba {
|
|
|
184
201
|
async generateUserAssignments(
|
|
185
202
|
userId: string,
|
|
186
203
|
segmentationData: SegmentationData,
|
|
187
|
-
): Promise<
|
|
204
|
+
): Promise<GeneratedUserAssignment[]> {
|
|
188
205
|
const experiments = await this.getActiveExperiments() // cached
|
|
189
206
|
const existingAssignments = await this.getAllExistingUserAssignments(userId)
|
|
190
207
|
|
|
191
|
-
const
|
|
192
|
-
const generatedAssignments: UserAssignment[] = []
|
|
193
|
-
|
|
208
|
+
const newAssignments: UserAssignment[] = []
|
|
194
209
|
for (const experiment of experiments) {
|
|
195
210
|
const existing = existingAssignments.find(ua => ua.experimentId === experiment.id)
|
|
196
|
-
if (existing) {
|
|
197
|
-
allAssignments.push(existing)
|
|
198
|
-
} else {
|
|
211
|
+
if (!existing) {
|
|
199
212
|
const assignment = this.generateUserAssignmentData(experiment, userId, segmentationData)
|
|
200
213
|
if (assignment) {
|
|
201
|
-
|
|
214
|
+
newAssignments.push(assignment)
|
|
202
215
|
}
|
|
203
216
|
}
|
|
204
217
|
}
|
|
205
218
|
|
|
206
|
-
await this.userAssignmentDao.saveBatch(
|
|
219
|
+
existingAssignments.push(...(await this.userAssignmentDao.saveBatch(newAssignments)))
|
|
207
220
|
|
|
208
|
-
|
|
221
|
+
const assignments: GeneratedUserAssignment[] = []
|
|
222
|
+
for (const experiment of experiments) {
|
|
223
|
+
const existing = existingAssignments.find(ua => ua.experimentId === experiment.id)
|
|
224
|
+
if (existing) {
|
|
225
|
+
assignments.push({
|
|
226
|
+
...existing,
|
|
227
|
+
experimentName: experiment.name,
|
|
228
|
+
bucketKey: experiment.buckets.find(i => i.id === existing.bucketId)?.key || null,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return assignments
|
|
209
233
|
}
|
|
210
234
|
|
|
211
235
|
/**
|
|
@@ -244,10 +268,12 @@ export class Abba {
|
|
|
244
268
|
const segmentationMatch = validateSegmentationRules(experiment.rules, segmentationData)
|
|
245
269
|
if (!segmentationMatch) return null
|
|
246
270
|
|
|
271
|
+
const bucket = determineAssignment(experiment.sampling, experiment.buckets)
|
|
272
|
+
|
|
247
273
|
return {
|
|
248
274
|
userId,
|
|
249
275
|
experimentId: experiment.id,
|
|
250
|
-
bucketId:
|
|
276
|
+
bucketId: bucket?.id || null,
|
|
251
277
|
}
|
|
252
278
|
}
|
|
253
279
|
|
package/src/types.ts
CHANGED
|
@@ -46,6 +46,11 @@ export type UserAssignment = BaseDBEntity<number> & {
|
|
|
46
46
|
bucketId: number | null
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export type GeneratedUserAssignment = Saved<UserAssignment> & {
|
|
50
|
+
experimentName: string
|
|
51
|
+
bucketKey: string | null
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
export type SegmentationData = Record<string, string | boolean | number>
|
|
50
55
|
|
|
51
56
|
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
|
/**
|