@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 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,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
- return existing;
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
- return await this.userAssignmentDao.save(assignment);
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 allAssignments = [];
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
- generatedAssignments.push(assignment);
148
+ newAssignments.push(assignment);
143
149
  }
144
150
  }
145
151
  }
146
- await this.userAssignmentDao.saveBatch(generatedAssignments);
147
- return [...allAssignments, ...generatedAssignments];
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: (0, util_1.determineAssignment)(experiment.sampling, experiment.buckets),
198
+ bucketId: bucket?.id || null,
182
199
  };
183
200
  }
184
201
  /**
@@ -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
- name: string;
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>[]) => 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.1",
3
+ "version": "1.11.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'
@@ -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<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
+ 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
- return await this.userAssignmentDao.save(assignment)
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<Saved<UserAssignment>[]> {
202
+ ): Promise<GeneratedUserAssignment[]> {
188
203
  const experiments = await this.getActiveExperiments() // cached
189
204
  const existingAssignments = await this.getAllExistingUserAssignments(userId)
190
205
 
191
- const allAssignments: Saved<UserAssignment>[] = []
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
- generatedAssignments.push(assignment)
212
+ newAssignments.push(assignment)
202
213
  }
203
214
  }
204
215
  }
205
216
 
206
- await this.userAssignmentDao.saveBatch(generatedAssignments)
217
+ existingAssignments.push(...(await this.userAssignmentDao.saveBatch(newAssignments)))
207
218
 
208
- return [...allAssignments, ...(generatedAssignments as Saved<UserAssignment>[])]
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: determineAssignment(experiment.sampling, experiment.buckets),
273
+ bucketId: bucket?.id || null,
251
274
  }
252
275
  }
253
276
 
@@ -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
- name: string
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>[]): 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
  /**