@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 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<Saved<UserAssignment> | null>;
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<Saved<UserAssignment>[]>;
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
- return existing;
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
- return await this.userAssignmentDao.save(assignment);
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 allAssignments = [];
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
- generatedAssignments.push(assignment);
150
+ newAssignments.push(assignment);
143
151
  }
144
152
  }
145
153
  }
146
- await this.userAssignmentDao.saveBatch(generatedAssignments);
147
- return [...allAssignments, ...generatedAssignments];
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: (0, util_1.determineAssignment)(experiment.sampling, experiment.buckets),
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>[]) => number | null;
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>[]) => number;
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
@@ -41,7 +41,7 @@ const determineBucket = (buckets) => {
41
41
  if (!bucket) {
42
42
  throw new Error('Could not detetermine bucket from ratios');
43
43
  }
44
- return bucket.id;
44
+ return bucket;
45
45
  };
46
46
  exports.determineBucket = determineBucket;
47
47
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/abba",
3
- "version": "1.9.2",
3
+ "version": "1.10.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build": "build",
package/readme.md CHANGED
@@ -154,7 +154,7 @@ async getUserAssignment(
154
154
  userId: string,
155
155
  existingOnly: boolean,
156
156
  segmentationData?: SegmentationData,
157
- ): Promise<Saved<UserAssignment> | null>
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<Saved<UserAssignment>[]>
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<Saved<UserAssignment> | null> {
149
+ ): Promise<GeneratedUserAssignment | null> {
149
150
  const existing = await this.getExistingUserAssignment(experimentId, userId)
150
- if (existing) return existing
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
- return await this.userAssignmentDao.save(assignment)
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<Saved<UserAssignment>[]> {
204
+ ): Promise<GeneratedUserAssignment[]> {
188
205
  const experiments = await this.getActiveExperiments() // cached
189
206
  const existingAssignments = await this.getAllExistingUserAssignments(userId)
190
207
 
191
- const allAssignments: Saved<UserAssignment>[] = []
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
- generatedAssignments.push(assignment)
214
+ newAssignments.push(assignment)
202
215
  }
203
216
  }
204
217
  }
205
218
 
206
- await this.userAssignmentDao.saveBatch(generatedAssignments)
219
+ existingAssignments.push(...(await this.userAssignmentDao.saveBatch(newAssignments)))
207
220
 
208
- return [...allAssignments, ...(generatedAssignments as Saved<UserAssignment>[])]
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: determineAssignment(experiment.sampling, experiment.buckets),
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>[]): number | null => {
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>[]): number => {
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.id
47
+ return bucket
48
48
  }
49
49
 
50
50
  /**