@naturalcycles/abba 2.0.1 → 2.1.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/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CommonDB } from '@naturalcycles/db-lib'
2
- import type { AnyObject, BaseDBEntity, IsoDate, Saved } from '@naturalcycles/js-lib'
2
+ import type { AnyObject, BaseDBEntity, IsoDate } from '@naturalcycles/js-lib'
3
3
 
4
4
  export interface AbbaConfig {
5
5
  db: CommonDB
@@ -31,6 +31,10 @@ export type BaseExperiment = BaseDBEntity & {
31
31
  * Date range end for the experiment assignments
32
32
  */
33
33
  endDateExcl: IsoDate
34
+ /**
35
+ * Whether the experiment is flagged as deleted. This acts as a soft delete only.
36
+ */
37
+ deleted: boolean
34
38
  }
35
39
 
36
40
  export type Experiment = BaseExperiment & {
@@ -39,8 +43,8 @@ export type Experiment = BaseExperiment & {
39
43
  data: AnyObject | null
40
44
  }
41
45
 
42
- export type ExperimentWithBuckets = Saved<Experiment> & {
43
- buckets: Saved<Bucket>[]
46
+ export type ExperimentWithBuckets = Experiment & {
47
+ buckets: Bucket[]
44
48
  }
45
49
 
46
50
  export type BaseBucket = BaseDBEntity & {
@@ -59,15 +63,11 @@ export type UserAssignment = BaseDBEntity & {
59
63
  bucketId: string | null
60
64
  }
61
65
 
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
+ export type DecoratedUserAssignment = UserAssignment & {
67
+ experimentKey: Experiment['key']
68
+ experimentData: Experiment['data']
69
+ bucketKey: Bucket['key'] | null
70
+ bucketData: Bucket['data']
71
71
  }
72
72
 
73
73
  export type SegmentationData = AnyObject
@@ -127,3 +127,7 @@ export interface BucketAssignmentStatistics {
127
127
  }
128
128
 
129
129
  export type ExclusionSet = Set<string>
130
+
131
+ export interface UserExperiment extends ExperimentWithBuckets {
132
+ userAssignment?: DecoratedUserAssignment
133
+ }
package/src/util.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  SegmentationRule,
11
11
  SegmentationRuleFn,
12
12
  UserAssignment,
13
+ UserExperiment,
13
14
  } from './types.js'
14
15
  import { AssignmentStatus, SegmentationRuleOperator } from './types.js'
15
16
 
@@ -17,11 +18,11 @@ import { AssignmentStatus, SegmentationRuleOperator } from './types.js'
17
18
  * Generate a new assignment for a given user.
18
19
  * Doesn't save it.
19
20
  */
20
- export const generateUserAssignmentData = (
21
+ export function generateUserAssignmentData(
21
22
  experiment: ExperimentWithBuckets,
22
23
  userId: string,
23
24
  segmentationData: SegmentationData,
24
- ): UserAssignment | null => {
25
+ ): Unsaved<UserAssignment> | null {
25
26
  const segmentationMatch = validateSegmentationRules(experiment.rules, segmentationData)
26
27
  if (!segmentationMatch) return null
27
28
 
@@ -31,7 +32,7 @@ export const generateUserAssignmentData = (
31
32
  userId,
32
33
  experimentId: experiment.id,
33
34
  bucketId: bucket?.id || null,
34
- } as UserAssignment
35
+ }
35
36
  }
36
37
 
37
38
  class RandomService {
@@ -48,7 +49,7 @@ export const randomService = new RandomService()
48
49
  /**
49
50
  * Determines a users assignment for this experiment. Returns null if they are not considered to be in the sampling group
50
51
  */
51
- export const determineAssignment = (sampling: number, buckets: Bucket[]): Bucket | null => {
52
+ export function determineAssignment(sampling: number, buckets: Bucket[]): Bucket | null {
52
53
  // Should this person be considered for the experiment?
53
54
  if (randomService.rollDie() > sampling) {
54
55
  return null
@@ -61,7 +62,7 @@ export const determineAssignment = (sampling: number, buckets: Bucket[]): Bucket
61
62
  /**
62
63
  * Determines which bucket a user assignment will recieve
63
64
  */
64
- export const determineBucket = (buckets: Bucket[]): Bucket => {
65
+ export function determineBucket(buckets: Bucket[]): Bucket {
65
66
  const bucketRoll = randomService.rollDie()
66
67
  let range: [number, number] | undefined
67
68
  const bucket = buckets.find(b => {
@@ -86,7 +87,7 @@ export const determineBucket = (buckets: Bucket[]): Bucket => {
86
87
  /**
87
88
  * Validate the total ratio of the buckets equals 100
88
89
  */
89
- export const validateTotalBucketRatio = (buckets: Unsaved<Bucket>[]): void => {
90
+ export function validateTotalBucketRatio(buckets: Unsaved<Bucket>[]): void {
90
91
  const bucketSum = buckets.reduce((sum, current) => sum + current.ratio, 0)
91
92
  if (bucketSum !== 100) {
92
93
  throw new Error('Total bucket ratio must be 100 before you can activate an experiment')
@@ -100,10 +101,10 @@ export const validateTotalBucketRatio = (buckets: Unsaved<Bucket>[]): void => {
100
101
  * @param segmentationData
101
102
  * @returns
102
103
  */
103
- export const validateSegmentationRules = (
104
+ export function validateSegmentationRules(
104
105
  rules: SegmentationRule[],
105
106
  segmentationData: SegmentationData,
106
- ): boolean => {
107
+ ): boolean {
107
108
  for (const rule of rules) {
108
109
  const { key, value, operator } = rule
109
110
  if (!segmentationRuleMap[operator](segmentationData[key], value)) return false
@@ -144,10 +145,10 @@ export const segmentationRuleMap: Record<SegmentationRuleOperator, SegmentationR
144
145
  /**
145
146
  * Returns true if an experiment is able to generate new assignments based on status and start/end dates
146
147
  */
147
- export const canGenerateNewAssignments = (
148
+ export function canGenerateNewAssignments(
148
149
  experiment: Experiment,
149
150
  exclusionSet: ExclusionSet,
150
- ): boolean => {
151
+ ): boolean {
151
152
  return (
152
153
  !exclusionSet.has(experiment.id) &&
153
154
  experiment.status === AssignmentStatus.Active &&
@@ -159,18 +160,15 @@ export const canGenerateNewAssignments = (
159
160
  * Returns an object that includes keys of all experimentIds a user should not be assigned to
160
161
  * based on a combination of existing assignments and mutual exclusion configuration
161
162
  */
162
- export const getUserExclusionSet = (
163
- experiments: Experiment[],
164
- existingAssignments: UserAssignment[],
165
- ): ExclusionSet => {
163
+ export function getUserExclusionSet(experiments: UserExperiment[]): ExclusionSet {
166
164
  const exclusionSet: ExclusionSet = new Set()
167
- existingAssignments.forEach(assignment => {
165
+ experiments.forEach(experiment => {
166
+ const { userAssignment } = experiment
168
167
  // Users who are excluded from an experiment due to sampling
169
168
  // should not prevent potential assignment to other mutually exclusive experiments
170
- if (assignment.bucketId === null) return
169
+ if (!userAssignment || userAssignment?.bucketId === null) return
171
170
 
172
- const experiment = experiments.find(e => e.id === assignment.experimentId)
173
- experiment?.exclusions.forEach(experimentId => exclusionSet.add(experimentId))
171
+ experiment.exclusions.forEach(experimentId => exclusionSet.add(experimentId))
174
172
  })
175
173
  return exclusionSet
176
174
  }