@naturalcycles/abba 1.24.0 → 1.25.1

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,6 +1,6 @@
1
1
  import { Unsaved } from '@naturalcycles/js-lib';
2
- import { AbbaConfig, Bucket, Experiment, ExperimentAssignmentStatistics, ExperimentWithBuckets, GeneratedUserAssignment } from './types';
3
2
  import { SegmentationData } from '.';
3
+ import { AbbaConfig, Bucket, Experiment, ExperimentAssignmentStatistics, ExperimentWithBuckets, GeneratedUserAssignment } from './types';
4
4
  export declare class Abba {
5
5
  cfg: AbbaConfig;
6
6
  private experimentDao;
@@ -48,7 +48,7 @@ export declare class Abba {
48
48
  * @param existingOnly Do not generate any new assignments for this experiment
49
49
  * @param segmentationData Required if existingOnly is false
50
50
  */
51
- getUserAssignment(experimentKey: string, userId: string, existingOnly: boolean, segmentationData?: SegmentationData): Promise<GeneratedUserAssignment | null>;
51
+ getUserAssignment(experimentKey: string, userId: string, existingOnly: boolean, segmentationData?: SegmentationData): Promise<GeneratedUserAssignment>;
52
52
  /**
53
53
  * Get all existing user assignments.
54
54
  * Hot method.
package/dist/abba.js CHANGED
@@ -32,7 +32,7 @@ class Abba {
32
32
  */
33
33
  async updateUserId(oldId, newId) {
34
34
  const query = this.userAssignmentDao.query().filterEq('userId', oldId);
35
- await this.userAssignmentDao.updateByQuery(query, { userId: newId });
35
+ await this.userAssignmentDao.patchByQuery(query, { userId: newId });
36
36
  }
37
37
  /**
38
38
  * Returns all experiments.
@@ -128,39 +128,61 @@ class Abba {
128
128
  const experiment = await this.experimentDao.getOneBy('key', experimentKey);
129
129
  (0, js_lib_1._assert)(experiment, `Experiment does not exist: ${experimentKey}`);
130
130
  // Inactive experiments should never return an assignment
131
- if (experiment.status === types_1.AssignmentStatus.Inactive)
132
- return null;
131
+ if (experiment.status === types_1.AssignmentStatus.Inactive) {
132
+ return {
133
+ experiment,
134
+ assignment: null,
135
+ };
136
+ }
133
137
  const buckets = await this.bucketDao.getBy('experimentId', experiment.id);
134
138
  const existingAssignments = await this.userAssignmentDao.getBy('userId', userId);
135
139
  const existing = existingAssignments.find(a => a.experimentId === experiment.id);
136
140
  if (existing) {
137
141
  const bucket = buckets.find(b => b.id === existing.bucketId);
138
142
  return {
139
- ...existing,
140
- experimentKey: experiment.key,
141
- bucketKey: bucket?.key || null,
142
- bucketData: bucket?.data || null,
143
+ experiment,
144
+ assignment: {
145
+ ...existing,
146
+ experimentKey: experiment.key,
147
+ bucketKey: bucket?.key || null,
148
+ bucketData: bucket?.data || null,
149
+ },
143
150
  };
144
151
  }
145
152
  // No existing assignment, but we don't want to generate a new one
146
- if (existingOnly || experiment.status === types_1.AssignmentStatus.Paused)
147
- return null;
153
+ if (existingOnly || experiment.status === types_1.AssignmentStatus.Paused) {
154
+ return {
155
+ experiment,
156
+ assignment: null,
157
+ };
158
+ }
148
159
  const experiments = await this.getAllExperiments();
149
160
  const exclusionSet = (0, util_1.getUserExclusionSet)(experiments, existingAssignments);
150
- if (!(0, util_1.canGenerateNewAssignments)(experiment, exclusionSet))
151
- return null;
161
+ if (!(0, util_1.canGenerateNewAssignments)(experiment, exclusionSet)) {
162
+ return {
163
+ experiment,
164
+ assignment: null,
165
+ };
166
+ }
152
167
  (0, js_lib_1._assert)(segmentationData, 'Segmentation data required when creating a new assignment');
153
168
  const experimentWithBuckets = { ...experiment, buckets };
154
169
  const assignment = (0, util_1.generateUserAssignmentData)(experimentWithBuckets, userId, segmentationData);
155
- if (!assignment)
156
- return null;
170
+ if (!assignment) {
171
+ return {
172
+ experiment,
173
+ assignment: null,
174
+ };
175
+ }
157
176
  const newAssignment = await this.userAssignmentDao.save(assignment);
158
177
  const bucket = buckets.find(b => b.id === newAssignment.bucketId);
159
178
  return {
160
- ...newAssignment,
161
- experimentKey: experiment.key,
162
- bucketKey: bucket?.key || null,
163
- bucketData: bucket?.data || null,
179
+ experiment,
180
+ assignment: {
181
+ ...newAssignment,
182
+ experimentKey: experiment.key,
183
+ bucketKey: bucket?.key || null,
184
+ bucketData: bucket?.data || null,
185
+ },
164
186
  };
165
187
  }
166
188
  /**
@@ -175,10 +197,13 @@ class Abba {
175
197
  const experiment = await this.experimentDao.requireById(assignment.experimentId);
176
198
  const bucket = await this.bucketDao.getById(assignment.bucketId);
177
199
  return {
178
- ...assignment,
179
- experimentKey: experiment.key,
180
- bucketKey: bucket?.key || null,
181
- bucketData: bucket?.data || null,
200
+ experiment,
201
+ assignment: {
202
+ ...assignment,
203
+ experimentKey: experiment.key,
204
+ bucketKey: bucket?.key || null,
205
+ bucketData: bucket?.data || null,
206
+ },
182
207
  };
183
208
  });
184
209
  }
@@ -202,10 +227,13 @@ class Abba {
202
227
  if (existing) {
203
228
  const bucket = experiment.buckets.find(b => b.id === existing.bucketId);
204
229
  assignments.push({
205
- ...existing,
206
- experimentKey: experiment.key,
207
- bucketKey: bucket?.key || null,
208
- bucketData: bucket?.data || null,
230
+ experiment,
231
+ assignment: {
232
+ ...existing,
233
+ experimentKey: experiment.key,
234
+ bucketKey: bucket?.key || null,
235
+ bucketData: bucket?.data || null,
236
+ },
209
237
  });
210
238
  }
211
239
  else if (!existingOnly && (0, util_1.canGenerateNewAssignments)(experiment, exclusionSet)) {
@@ -215,10 +243,13 @@ class Abba {
215
243
  newAssignments.push(created);
216
244
  const bucket = experiment.buckets.find(b => b.id === created.bucketId);
217
245
  assignments.push({
218
- ...created,
219
- experimentKey: experiment.key,
220
- bucketKey: bucket?.key || null,
221
- bucketData: bucket?.data || null,
246
+ experiment,
247
+ assignment: {
248
+ ...created,
249
+ experimentKey: experiment.key,
250
+ bucketKey: bucket?.key || null,
251
+ bucketData: bucket?.data || null,
252
+ },
222
253
  });
223
254
  // Prevent future exclusion clashes
224
255
  experiment.exclusions.forEach(experimentId => exclusionSet.add(experimentId));
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from './types';
2
1
  export * from './abba';
2
+ export * from './types';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
- tslib_1.__exportStar(require("./types"), exports);
5
4
  tslib_1.__exportStar(require("./abba"), exports);
5
+ tslib_1.__exportStar(require("./types"), exports);
package/dist/types.d.ts CHANGED
@@ -51,11 +51,14 @@ export type UserAssignment = BaseDBEntity & {
51
51
  experimentId: string;
52
52
  bucketId: string | null;
53
53
  };
54
- export type GeneratedUserAssignment = Saved<UserAssignment> & {
55
- experimentKey: string;
56
- bucketKey: string | null;
57
- bucketData: AnyObject | null;
58
- };
54
+ export interface GeneratedUserAssignment {
55
+ assignment: (Saved<UserAssignment> & {
56
+ experimentKey: string;
57
+ bucketKey: string | null;
58
+ bucketData: AnyObject | null;
59
+ }) | null;
60
+ experiment: Saved<Experiment>;
61
+ }
59
62
  export type SegmentationData = AnyObject;
60
63
  export declare enum AssignmentStatus {
61
64
  /**
package/dist/util.js CHANGED
@@ -108,7 +108,7 @@ exports.segmentationRuleMap = {
108
108
  return (0, semver_1.satisfies)(keyValue?.toString() || '', ruleValue.toString());
109
109
  },
110
110
  [types_1.SegmentationRuleOperator.Regex](keyValue, ruleValue) {
111
- return new RegExp(`${ruleValue}`).test(keyValue?.toString() || '');
111
+ return new RegExp(ruleValue).test(keyValue?.toString() || '');
112
112
  },
113
113
  [types_1.SegmentationRuleOperator.Boolean](keyValue, ruleValue) {
114
114
  // If it's true, then must be true
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@naturalcycles/abba",
3
- "version": "1.24.0",
3
+ "version": "1.25.1",
4
4
  "scripts": {
5
- "prepare": "husky"
5
+ "prepare": "husky",
6
+ "build": "dev-lib build",
7
+ "test": "dev-lib test",
8
+ "lint": "dev-lib lint",
9
+ "bt": "dev-lib bt",
10
+ "lbt": "dev-lib lbt"
6
11
  },
7
12
  "dependencies": {
8
13
  "@naturalcycles/db-lib": "^9.14.1",
@@ -11,8 +16,8 @@
11
16
  "semver": "^7.3.5"
12
17
  },
13
18
  "devDependencies": {
14
- "@naturalcycles/dev-lib": "^13.44.8",
15
- "@types/node": "^20.2.4",
19
+ "@naturalcycles/dev-lib": "^15.19.0",
20
+ "@types/node": "^22.7.4",
16
21
  "@types/semver": "^7.3.9",
17
22
  "jest": "^29.3.1"
18
23
  },
@@ -34,7 +39,7 @@
34
39
  "url": "https://github.com/NaturalCycles/abba"
35
40
  },
36
41
  "engines": {
37
- "node": ">=18.12.0"
42
+ "node": ">=20.13.0"
38
43
  },
39
44
  "description": "AB test assignment configuration tool for Node.js",
40
45
  "author": "Natural Cycles Team",
package/src/abba.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { _assert, _Memo, _shuffle, pMap, Unsaved } from '@naturalcycles/js-lib'
2
2
  import { LRUMemoCache } from '@naturalcycles/nodejs-lib'
3
+ import { SegmentationData } from '.'
3
4
  import { bucketDao } from './dao/bucket.dao'
4
5
  import { experimentDao } from './dao/experiment.dao'
5
6
  import { userAssignmentDao } from './dao/userAssignment.dao'
@@ -20,7 +21,6 @@ import {
20
21
  getUserExclusionSet,
21
22
  validateTotalBucketRatio,
22
23
  } from './util'
23
- import { SegmentationData } from '.'
24
24
 
25
25
  /**
26
26
  * 10 minutes
@@ -48,7 +48,7 @@ export class Abba {
48
48
  */
49
49
  async updateUserId(oldId: string, newId: string): Promise<void> {
50
50
  const query = this.userAssignmentDao.query().filterEq('userId', oldId)
51
- await this.userAssignmentDao.updateByQuery(query, { userId: newId })
51
+ await this.userAssignmentDao.patchByQuery(query, { userId: newId })
52
52
  }
53
53
 
54
54
  /**
@@ -178,12 +178,17 @@ export class Abba {
178
178
  userId: string,
179
179
  existingOnly: boolean,
180
180
  segmentationData?: SegmentationData,
181
- ): Promise<GeneratedUserAssignment | null> {
181
+ ): Promise<GeneratedUserAssignment> {
182
182
  const experiment = await this.experimentDao.getOneBy('key', experimentKey)
183
183
  _assert(experiment, `Experiment does not exist: ${experimentKey}`)
184
184
 
185
185
  // Inactive experiments should never return an assignment
186
- if (experiment.status === AssignmentStatus.Inactive) return null
186
+ if (experiment.status === AssignmentStatus.Inactive) {
187
+ return {
188
+ experiment,
189
+ assignment: null,
190
+ }
191
+ }
187
192
 
188
193
  const buckets = await this.bucketDao.getBy('experimentId', experiment.id)
189
194
  const existingAssignments = await this.userAssignmentDao.getBy('userId', userId)
@@ -191,35 +196,56 @@ export class Abba {
191
196
  if (existing) {
192
197
  const bucket = buckets.find(b => b.id === existing.bucketId)
193
198
  return {
194
- ...existing,
195
- experimentKey: experiment.key,
196
- bucketKey: bucket?.key || null,
197
- bucketData: bucket?.data || null,
199
+ experiment,
200
+ assignment: {
201
+ ...existing,
202
+ experimentKey: experiment.key,
203
+ bucketKey: bucket?.key || null,
204
+ bucketData: bucket?.data || null,
205
+ },
198
206
  }
199
207
  }
200
208
 
201
209
  // No existing assignment, but we don't want to generate a new one
202
- if (existingOnly || experiment.status === AssignmentStatus.Paused) return null
210
+ if (existingOnly || experiment.status === AssignmentStatus.Paused) {
211
+ return {
212
+ experiment,
213
+ assignment: null,
214
+ }
215
+ }
203
216
 
204
217
  const experiments = await this.getAllExperiments()
205
218
  const exclusionSet = getUserExclusionSet(experiments, existingAssignments)
206
- if (!canGenerateNewAssignments(experiment, exclusionSet)) return null
219
+ if (!canGenerateNewAssignments(experiment, exclusionSet)) {
220
+ return {
221
+ experiment,
222
+ assignment: null,
223
+ }
224
+ }
207
225
 
208
226
  _assert(segmentationData, 'Segmentation data required when creating a new assignment')
209
227
 
210
228
  const experimentWithBuckets = { ...experiment, buckets }
211
229
  const assignment = generateUserAssignmentData(experimentWithBuckets, userId, segmentationData)
212
- if (!assignment) return null
230
+ if (!assignment) {
231
+ return {
232
+ experiment,
233
+ assignment: null,
234
+ }
235
+ }
213
236
 
214
237
  const newAssignment = await this.userAssignmentDao.save(assignment)
215
238
 
216
239
  const bucket = buckets.find(b => b.id === newAssignment.bucketId)
217
240
 
218
241
  return {
219
- ...newAssignment,
220
- experimentKey: experiment.key,
221
- bucketKey: bucket?.key || null,
222
- bucketData: bucket?.data || null,
242
+ experiment,
243
+ assignment: {
244
+ ...newAssignment,
245
+ experimentKey: experiment.key,
246
+ bucketKey: bucket?.key || null,
247
+ bucketData: bucket?.data || null,
248
+ },
223
249
  }
224
250
  }
225
251
 
@@ -235,10 +261,13 @@ export class Abba {
235
261
  const experiment = await this.experimentDao.requireById(assignment.experimentId)
236
262
  const bucket = await this.bucketDao.getById(assignment.bucketId)
237
263
  return {
238
- ...assignment,
239
- experimentKey: experiment.key,
240
- bucketKey: bucket?.key || null,
241
- bucketData: bucket?.data || null,
264
+ experiment,
265
+ assignment: {
266
+ ...assignment,
267
+ experimentKey: experiment.key,
268
+ bucketKey: bucket?.key || null,
269
+ bucketData: bucket?.data || null,
270
+ },
242
271
  }
243
272
  })
244
273
  }
@@ -273,10 +302,13 @@ export class Abba {
273
302
  if (existing) {
274
303
  const bucket = experiment.buckets.find(b => b.id === existing.bucketId)
275
304
  assignments.push({
276
- ...existing,
277
- experimentKey: experiment.key,
278
- bucketKey: bucket?.key || null,
279
- bucketData: bucket?.data || null,
305
+ experiment,
306
+ assignment: {
307
+ ...existing,
308
+ experimentKey: experiment.key,
309
+ bucketKey: bucket?.key || null,
310
+ bucketData: bucket?.data || null,
311
+ },
280
312
  })
281
313
  } else if (!existingOnly && canGenerateNewAssignments(experiment, exclusionSet)) {
282
314
  const assignment = generateUserAssignmentData(experiment, userId, segmentationData)
@@ -285,10 +317,13 @@ export class Abba {
285
317
  newAssignments.push(created)
286
318
  const bucket = experiment.buckets.find(b => b.id === created.bucketId)
287
319
  assignments.push({
288
- ...created,
289
- experimentKey: experiment.key,
290
- bucketKey: bucket?.key || null,
291
- bucketData: bucket?.data || null,
320
+ experiment,
321
+ assignment: {
322
+ ...created,
323
+ experimentKey: experiment.key,
324
+ bucketKey: bucket?.key || null,
325
+ bucketData: bucket?.data || null,
326
+ },
292
327
  })
293
328
  // Prevent future exclusion clashes
294
329
  experiment.exclusions.forEach(experimentId => exclusionSet.add(experimentId))
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from './types'
2
1
  export * from './abba'
2
+ export * from './types'
package/src/types.ts CHANGED
@@ -59,10 +59,15 @@ export type UserAssignment = BaseDBEntity & {
59
59
  bucketId: string | null
60
60
  }
61
61
 
62
- export type GeneratedUserAssignment = Saved<UserAssignment> & {
63
- experimentKey: string
64
- bucketKey: string | null
65
- bucketData: AnyObject | null
62
+ export interface GeneratedUserAssignment {
63
+ assignment:
64
+ | (Saved<UserAssignment> & {
65
+ experimentKey: string
66
+ bucketKey: string | null
67
+ bucketData: AnyObject | null
68
+ })
69
+ | null
70
+ experiment: Saved<Experiment>
66
71
  }
67
72
 
68
73
  export type SegmentationData = AnyObject
package/src/util.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Unsaved, localDate } from '@naturalcycles/js-lib'
1
+ import { localDate, Unsaved } from '@naturalcycles/js-lib'
2
2
  import { satisfies } from 'semver'
3
3
  import {
4
4
  AssignmentStatus,
@@ -127,7 +127,7 @@ export const segmentationRuleMap: Record<SegmentationRuleOperator, SegmentationR
127
127
  return satisfies(keyValue?.toString() || '', ruleValue.toString())
128
128
  },
129
129
  [SegmentationRuleOperator.Regex](keyValue, ruleValue) {
130
- return new RegExp(`${ruleValue}`).test(keyValue?.toString() || '')
130
+ return new RegExp(ruleValue).test(keyValue?.toString() || '')
131
131
  },
132
132
  [SegmentationRuleOperator.Boolean](keyValue, ruleValue) {
133
133
  // If it's true, then must be true