@lssm/lib.feature-flags 0.0.0-canary-20251217083314 → 1.41.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/contracts/index.js +1 -910
- package/dist/docs/feature-flags.docblock.js +2 -21
- package/dist/docs/index.js +1 -1
- package/dist/entities/index.js +1 -325
- package/dist/evaluation/index.js +1 -220
- package/dist/events.js +1 -459
- package/dist/feature-flags.feature.js +1 -139
- package/dist/index.js +1 -8
- package/package.json +13 -13
- package/dist/contracts/dist/capabilities/openbanking.js +0 -88
- package/dist/contracts/dist/client/index.js +0 -5
- package/dist/contracts/dist/client/react/feature-render.js +0 -2
- package/dist/contracts/dist/client/react/form-render.js +0 -4
- package/dist/contracts/dist/client/react/index.js +0 -4
- package/dist/contracts/dist/contract-registry/index.js +0 -1
- package/dist/contracts/dist/contract-registry/schemas.js +0 -60
- package/dist/contracts/dist/docs/PUBLISHING.docblock.js +0 -16
- package/dist/contracts/dist/docs/accessibility_wcag_compliance_specs.docblock.js +0 -16
- package/dist/contracts/dist/docs/index.js +0 -29
- package/dist/contracts/dist/docs/presentations.js +0 -71
- package/dist/contracts/dist/docs/registry.js +0 -44
- package/dist/contracts/dist/docs/tech/PHASE_1_QUICKSTART.docblock.js +0 -16
- package/dist/contracts/dist/docs/tech/PHASE_2_AI_NATIVE_OPERATIONS.docblock.js +0 -16
- package/dist/contracts/dist/docs/tech/PHASE_3_AUTO_EVOLUTION.docblock.js +0 -16
- package/dist/contracts/dist/docs/tech/PHASE_4_PERSONALIZATION_ENGINE.docblock.js +0 -16
- package/dist/contracts/dist/docs/tech/PHASE_5_ZERO_TOUCH_OPERATIONS.docblock.js +0 -16
- package/dist/contracts/dist/docs/tech/auth/better-auth-nextjs.docblock.js +0 -80
- package/dist/contracts/dist/docs/tech/contracts/openapi-export.docblock.js +0 -57
- package/dist/contracts/dist/docs/tech/lifecycle-stage-system.docblock.js +0 -16
- package/dist/contracts/dist/docs/tech/llm/llm-integration.docblock.js +0 -357
- package/dist/contracts/dist/docs/tech/mcp-endpoints.docblock.js +0 -37
- package/dist/contracts/dist/docs/tech/presentation-runtime.docblock.js +0 -16
- package/dist/contracts/dist/docs/tech/schema/README.docblock.js +0 -20
- package/dist/contracts/dist/docs/tech/studio/learning-events.docblock.js +0 -48
- package/dist/contracts/dist/docs/tech/studio/learning-journeys.docblock.js +0 -79
- package/dist/contracts/dist/docs/tech/studio/platform-admin-panel.docblock.js +0 -84
- package/dist/contracts/dist/docs/tech/studio/project-access-teams.docblock.js +0 -45
- package/dist/contracts/dist/docs/tech/studio/project-routing.docblock.js +0 -67
- package/dist/contracts/dist/docs/tech/studio/sandbox-unlogged.docblock.js +0 -40
- package/dist/contracts/dist/docs/tech/studio/team-invitations.docblock.js +0 -69
- package/dist/contracts/dist/docs/tech/studio/workspace-ops.docblock.js +0 -47
- package/dist/contracts/dist/docs/tech/studio/workspaces.docblock.js +0 -62
- package/dist/contracts/dist/docs/tech/telemetry-ingest.docblock.js +0 -155
- package/dist/contracts/dist/docs/tech/templates/runtime.docblock.js +0 -20
- package/dist/contracts/dist/docs/tech/vscode-extension.docblock.js +0 -101
- package/dist/contracts/dist/docs/tech/workflows/overview.docblock.js +0 -20
- package/dist/contracts/dist/events.js +0 -10
- package/dist/contracts/dist/experiments/evaluator.js +0 -1
- package/dist/contracts/dist/index.js +0 -71
- package/dist/contracts/dist/install.js +0 -2
- package/dist/contracts/dist/integrations/contracts.js +0 -377
- package/dist/contracts/dist/integrations/index.js +0 -18
- package/dist/contracts/dist/integrations/openbanking/contracts/accounts.js +0 -228
- package/dist/contracts/dist/integrations/openbanking/contracts/balances.js +0 -159
- package/dist/contracts/dist/integrations/openbanking/contracts/index.js +0 -3
- package/dist/contracts/dist/integrations/openbanking/contracts/transactions.js +0 -210
- package/dist/contracts/dist/integrations/openbanking/models.js +0 -242
- package/dist/contracts/dist/integrations/openbanking/telemetry.js +0 -13
- package/dist/contracts/dist/integrations/providers/elevenlabs.js +0 -52
- package/dist/contracts/dist/integrations/providers/gcs-storage.js +0 -75
- package/dist/contracts/dist/integrations/providers/gmail.js +0 -87
- package/dist/contracts/dist/integrations/providers/google-calendar.js +0 -66
- package/dist/contracts/dist/integrations/providers/index.js +0 -11
- package/dist/contracts/dist/integrations/providers/mistral.js +0 -68
- package/dist/contracts/dist/integrations/providers/postmark.js +0 -68
- package/dist/contracts/dist/integrations/providers/powens.js +0 -116
- package/dist/contracts/dist/integrations/providers/qdrant.js +0 -73
- package/dist/contracts/dist/integrations/providers/registry.js +0 -10
- package/dist/contracts/dist/integrations/providers/stripe.js +0 -83
- package/dist/contracts/dist/integrations/providers/twilio-sms.js +0 -61
- package/dist/contracts/dist/jsonschema.js +0 -1
- package/dist/contracts/dist/knowledge/contracts.js +0 -306
- package/dist/contracts/dist/knowledge/index.js +0 -7
- package/dist/contracts/dist/knowledge/spaces/email-threads.js +0 -34
- package/dist/contracts/dist/knowledge/spaces/financial-docs.js +0 -34
- package/dist/contracts/dist/knowledge/spaces/financial-overview.js +0 -38
- package/dist/contracts/dist/knowledge/spaces/index.js +0 -6
- package/dist/contracts/dist/knowledge/spaces/product-canon.js +0 -34
- package/dist/contracts/dist/knowledge/spaces/support-faq.js +0 -37
- package/dist/contracts/dist/knowledge/spaces/uploaded-docs.js +0 -34
- package/dist/contracts/dist/llm/exporters.js +0 -19
- package/dist/contracts/dist/llm/index.js +0 -2
- package/dist/contracts/dist/llm/prompts.js +0 -1
- package/dist/contracts/dist/onboarding-base.js +0 -196
- package/dist/contracts/dist/openapi.js +0 -1
- package/dist/contracts/dist/ownership.js +0 -21
- package/dist/contracts/dist/presentations.js +0 -1
- package/dist/contracts/dist/presentations.v2.js +0 -11
- package/dist/contracts/dist/prompt.js +0 -1
- package/dist/contracts/dist/promptRegistry.js +0 -1
- package/dist/contracts/dist/regenerator/index.js +0 -1
- package/dist/contracts/dist/regenerator/service.js +0 -6
- package/dist/contracts/dist/registry.js +0 -2
- package/dist/contracts/dist/resources.js +0 -1
- package/dist/contracts/dist/schema/dist/EnumType.js +0 -2
- package/dist/contracts/dist/schema/dist/FieldType.js +0 -49
- package/dist/contracts/dist/schema/dist/ScalarTypeEnum.js +0 -236
- package/dist/contracts/dist/schema/dist/SchemaModel.js +0 -34
- package/dist/contracts/dist/schema/dist/entity/defineEntity.js +0 -1
- package/dist/contracts/dist/schema/dist/entity/index.js +0 -2
- package/dist/contracts/dist/schema/dist/entity/types.js +0 -1
- package/dist/contracts/dist/schema/dist/index.js +0 -6
- package/dist/contracts/dist/server/graphql-pothos.js +0 -6
- package/dist/contracts/dist/server/index.js +0 -8
- package/dist/contracts/dist/server/mcp/createMcpServer.js +0 -4
- package/dist/contracts/dist/server/mcp/registerPresentations.js +0 -2
- package/dist/contracts/dist/server/mcp/registerPrompts.js +0 -1
- package/dist/contracts/dist/server/mcp/registerResources.js +0 -2
- package/dist/contracts/dist/server/mcp/registerTools.js +0 -1
- package/dist/contracts/dist/server/provider-mcp.js +0 -1
- package/dist/contracts/dist/server/rest-elysia.js +0 -1
- package/dist/contracts/dist/server/rest-express.js +0 -1
- package/dist/contracts/dist/server/rest-generic.js +0 -1
- package/dist/contracts/dist/server/rest-next-app.js +0 -1
- package/dist/contracts/dist/server/rest-next-pages.js +0 -1
- package/dist/contracts/dist/spec.js +0 -34
- package/dist/contracts/dist/telemetry/index.js +0 -1
- package/dist/contracts/dist/telemetry/tracker.js +0 -1
- package/dist/contracts/dist/tests/index.js +0 -1
- package/dist/contracts/dist/tests/runner.js +0 -2
- package/dist/contracts/dist/workflow/index.js +0 -1
- package/dist/contracts/dist/workflow/runner.js +0 -1
- package/dist/contracts/index.d.ts +0 -1006
- package/dist/docs/feature-flags.docblock.d.ts +0 -1
- package/dist/docs/index.d.ts +0 -1
- package/dist/entities/index.d.ts +0 -200
- package/dist/evaluation/index.d.ts +0 -162
- package/dist/events.d.ts +0 -624
- package/dist/feature-flags.feature.d.ts +0 -11
- package/dist/index.d.ts +0 -6
- package/dist/schema/dist/EnumType.js +0 -2
- package/dist/schema/dist/FieldType.js +0 -49
- package/dist/schema/dist/ScalarTypeEnum.js +0 -236
- package/dist/schema/dist/SchemaModel.js +0 -39
- package/dist/schema/dist/entity/defineEntity.js +0 -236
- package/dist/schema/dist/entity/index.js +0 -2
- package/dist/schema/dist/entity/types.js +0 -1
- package/dist/schema/dist/index.js +0 -6
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import "../contracts/dist/docs/index.js";
|
|
3
|
-
|
|
4
|
-
//#region src/docs/feature-flags.docblock.ts
|
|
5
|
-
registerDocBlocks([{
|
|
6
|
-
id: "docs.feature-flags.overview",
|
|
7
|
-
title: "Feature Flags & Experiments",
|
|
8
|
-
summary: "Reusable, spec-first feature flag and experiment module with targeting, gradual rollout, multivariate variants, and evaluation logging.",
|
|
9
|
-
kind: "reference",
|
|
10
|
-
visibility: "public",
|
|
11
|
-
route: "/docs/feature-flags/overview",
|
|
12
|
-
tags: [
|
|
13
|
-
"feature-flags",
|
|
14
|
-
"experiments",
|
|
15
|
-
"progressive-delivery"
|
|
16
|
-
],
|
|
17
|
-
body: `## What this module provides
|
|
1
|
+
import{registerDocBlocks as e}from"@lssm/lib.contracts/docs";e([{id:`docs.feature-flags.overview`,title:`Feature Flags & Experiments`,summary:`Reusable, spec-first feature flag and experiment module with targeting, gradual rollout, multivariate variants, and evaluation logging.`,kind:`reference`,visibility:`public`,route:`/docs/feature-flags/overview`,tags:[`feature-flags`,`experiments`,`progressive-delivery`],body:`## What this module provides
|
|
18
2
|
|
|
19
3
|
- **Entities**: FeatureFlag, FlagTargetingRule, Experiment, ExperimentAssignment, FlagEvaluation.
|
|
20
4
|
- **Contracts**: create/update/delete/toggle/list/get flags; create/delete rules; evaluate flags; create/start/stop/get experiments.
|
|
@@ -70,7 +54,4 @@ if (result.enabled) {
|
|
|
70
54
|
- Ensure experiments’ variant percentages sum to 100; default flag status to OFF.
|
|
71
55
|
- Use org-scoped flags for multi-tenant isolation.
|
|
72
56
|
- Log evaluations only when needed to control volume; prefer sampling for noisy paths.
|
|
73
|
-
`
|
|
74
|
-
}]);
|
|
75
|
-
|
|
76
|
-
//#endregion
|
|
57
|
+
`}]);
|
package/dist/docs/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import"./feature-flags.docblock.js";
|
package/dist/entities/index.js
CHANGED
|
@@ -1,325 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import "../schema/dist/index.js";
|
|
3
|
-
|
|
4
|
-
//#region src/entities/index.ts
|
|
5
|
-
/**
|
|
6
|
-
* Feature flag status enum.
|
|
7
|
-
*/
|
|
8
|
-
const FlagStatusEnum = defineEntityEnum({
|
|
9
|
-
name: "FlagStatus",
|
|
10
|
-
values: [
|
|
11
|
-
"OFF",
|
|
12
|
-
"ON",
|
|
13
|
-
"GRADUAL"
|
|
14
|
-
],
|
|
15
|
-
schema: "lssm_feature_flags",
|
|
16
|
-
description: "Status of a feature flag."
|
|
17
|
-
});
|
|
18
|
-
/**
|
|
19
|
-
* Targeting rule operator enum.
|
|
20
|
-
*/
|
|
21
|
-
const RuleOperatorEnum = defineEntityEnum({
|
|
22
|
-
name: "RuleOperator",
|
|
23
|
-
values: [
|
|
24
|
-
"EQ",
|
|
25
|
-
"NEQ",
|
|
26
|
-
"IN",
|
|
27
|
-
"NIN",
|
|
28
|
-
"CONTAINS",
|
|
29
|
-
"NOT_CONTAINS",
|
|
30
|
-
"GT",
|
|
31
|
-
"GTE",
|
|
32
|
-
"LT",
|
|
33
|
-
"LTE",
|
|
34
|
-
"PERCENTAGE"
|
|
35
|
-
],
|
|
36
|
-
schema: "lssm_feature_flags",
|
|
37
|
-
description: "Operator for targeting rule conditions."
|
|
38
|
-
});
|
|
39
|
-
/**
|
|
40
|
-
* Experiment status enum.
|
|
41
|
-
*/
|
|
42
|
-
const ExperimentStatusEnum = defineEntityEnum({
|
|
43
|
-
name: "ExperimentStatus",
|
|
44
|
-
values: [
|
|
45
|
-
"DRAFT",
|
|
46
|
-
"RUNNING",
|
|
47
|
-
"PAUSED",
|
|
48
|
-
"COMPLETED",
|
|
49
|
-
"CANCELLED"
|
|
50
|
-
],
|
|
51
|
-
schema: "lssm_feature_flags",
|
|
52
|
-
description: "Status of an experiment."
|
|
53
|
-
});
|
|
54
|
-
/**
|
|
55
|
-
* FeatureFlag entity - defines a feature flag.
|
|
56
|
-
*/
|
|
57
|
-
const FeatureFlagEntity = defineEntity({
|
|
58
|
-
name: "FeatureFlag",
|
|
59
|
-
description: "A feature flag for controlling feature availability.",
|
|
60
|
-
schema: "lssm_feature_flags",
|
|
61
|
-
map: "feature_flag",
|
|
62
|
-
fields: {
|
|
63
|
-
id: field.id({ description: "Unique flag identifier" }),
|
|
64
|
-
key: field.string({
|
|
65
|
-
isUnique: true,
|
|
66
|
-
description: "Flag key (e.g., new_dashboard)"
|
|
67
|
-
}),
|
|
68
|
-
name: field.string({ description: "Human-readable name" }),
|
|
69
|
-
description: field.string({
|
|
70
|
-
isOptional: true,
|
|
71
|
-
description: "Description of the flag"
|
|
72
|
-
}),
|
|
73
|
-
status: field.enum("FlagStatus", {
|
|
74
|
-
default: "OFF",
|
|
75
|
-
description: "Flag status"
|
|
76
|
-
}),
|
|
77
|
-
defaultValue: field.boolean({
|
|
78
|
-
default: false,
|
|
79
|
-
description: "Default value when no rules match"
|
|
80
|
-
}),
|
|
81
|
-
variants: field.json({
|
|
82
|
-
isOptional: true,
|
|
83
|
-
description: "Variant definitions for multivariate flags"
|
|
84
|
-
}),
|
|
85
|
-
orgId: field.string({
|
|
86
|
-
isOptional: true,
|
|
87
|
-
description: "Organization scope (null = global)"
|
|
88
|
-
}),
|
|
89
|
-
tags: field.json({
|
|
90
|
-
isOptional: true,
|
|
91
|
-
description: "Tags for categorization"
|
|
92
|
-
}),
|
|
93
|
-
metadata: field.json({
|
|
94
|
-
isOptional: true,
|
|
95
|
-
description: "Additional metadata"
|
|
96
|
-
}),
|
|
97
|
-
createdAt: field.createdAt(),
|
|
98
|
-
updatedAt: field.updatedAt(),
|
|
99
|
-
targetingRules: field.hasMany("FlagTargetingRule"),
|
|
100
|
-
experiments: field.hasMany("Experiment"),
|
|
101
|
-
evaluations: field.hasMany("FlagEvaluation")
|
|
102
|
-
},
|
|
103
|
-
indexes: [index.on(["orgId", "key"]), index.on(["status"])],
|
|
104
|
-
enums: [FlagStatusEnum]
|
|
105
|
-
});
|
|
106
|
-
/**
|
|
107
|
-
* FlagTargetingRule entity - conditions for targeting.
|
|
108
|
-
*/
|
|
109
|
-
const FlagTargetingRuleEntity = defineEntity({
|
|
110
|
-
name: "FlagTargetingRule",
|
|
111
|
-
description: "A targeting rule for conditional flag evaluation.",
|
|
112
|
-
schema: "lssm_feature_flags",
|
|
113
|
-
map: "flag_targeting_rule",
|
|
114
|
-
fields: {
|
|
115
|
-
id: field.id({ description: "Unique rule identifier" }),
|
|
116
|
-
flagId: field.foreignKey({ description: "Parent feature flag" }),
|
|
117
|
-
name: field.string({
|
|
118
|
-
isOptional: true,
|
|
119
|
-
description: "Rule name for debugging"
|
|
120
|
-
}),
|
|
121
|
-
priority: field.int({
|
|
122
|
-
default: 0,
|
|
123
|
-
description: "Rule priority (lower = higher priority)"
|
|
124
|
-
}),
|
|
125
|
-
enabled: field.boolean({
|
|
126
|
-
default: true,
|
|
127
|
-
description: "Whether rule is active"
|
|
128
|
-
}),
|
|
129
|
-
attribute: field.string({ description: "Target attribute (userId, orgId, plan, segment, etc.)" }),
|
|
130
|
-
operator: field.enum("RuleOperator", { description: "Comparison operator" }),
|
|
131
|
-
value: field.json({ description: "Target value(s)" }),
|
|
132
|
-
rolloutPercentage: field.int({
|
|
133
|
-
isOptional: true,
|
|
134
|
-
description: "Percentage for gradual rollout (0-100)"
|
|
135
|
-
}),
|
|
136
|
-
serveValue: field.boolean({
|
|
137
|
-
isOptional: true,
|
|
138
|
-
description: "Boolean value to serve"
|
|
139
|
-
}),
|
|
140
|
-
serveVariant: field.string({
|
|
141
|
-
isOptional: true,
|
|
142
|
-
description: "Variant key to serve (for multivariate)"
|
|
143
|
-
}),
|
|
144
|
-
createdAt: field.createdAt(),
|
|
145
|
-
updatedAt: field.updatedAt(),
|
|
146
|
-
flag: field.belongsTo("FeatureFlag", ["flagId"], ["id"], { onDelete: "Cascade" })
|
|
147
|
-
},
|
|
148
|
-
indexes: [index.on(["flagId", "priority"]), index.on(["attribute"])],
|
|
149
|
-
enums: [RuleOperatorEnum]
|
|
150
|
-
});
|
|
151
|
-
/**
|
|
152
|
-
* Experiment entity - A/B test configuration.
|
|
153
|
-
*/
|
|
154
|
-
const ExperimentEntity = defineEntity({
|
|
155
|
-
name: "Experiment",
|
|
156
|
-
description: "An A/B test experiment.",
|
|
157
|
-
schema: "lssm_feature_flags",
|
|
158
|
-
map: "experiment",
|
|
159
|
-
fields: {
|
|
160
|
-
id: field.id({ description: "Unique experiment identifier" }),
|
|
161
|
-
key: field.string({
|
|
162
|
-
isUnique: true,
|
|
163
|
-
description: "Experiment key"
|
|
164
|
-
}),
|
|
165
|
-
name: field.string({ description: "Human-readable name" }),
|
|
166
|
-
description: field.string({
|
|
167
|
-
isOptional: true,
|
|
168
|
-
description: "Experiment description"
|
|
169
|
-
}),
|
|
170
|
-
hypothesis: field.string({
|
|
171
|
-
isOptional: true,
|
|
172
|
-
description: "Experiment hypothesis"
|
|
173
|
-
}),
|
|
174
|
-
flagId: field.foreignKey({ description: "Associated feature flag" }),
|
|
175
|
-
status: field.enum("ExperimentStatus", {
|
|
176
|
-
default: "DRAFT",
|
|
177
|
-
description: "Experiment status"
|
|
178
|
-
}),
|
|
179
|
-
variants: field.json({ description: "Variant definitions with split ratios" }),
|
|
180
|
-
metrics: field.json({
|
|
181
|
-
isOptional: true,
|
|
182
|
-
description: "Metrics to track"
|
|
183
|
-
}),
|
|
184
|
-
audiencePercentage: field.int({
|
|
185
|
-
default: 100,
|
|
186
|
-
description: "Percentage of audience to include"
|
|
187
|
-
}),
|
|
188
|
-
audienceFilter: field.json({
|
|
189
|
-
isOptional: true,
|
|
190
|
-
description: "Audience filter criteria"
|
|
191
|
-
}),
|
|
192
|
-
scheduledStartAt: field.dateTime({
|
|
193
|
-
isOptional: true,
|
|
194
|
-
description: "Scheduled start time"
|
|
195
|
-
}),
|
|
196
|
-
scheduledEndAt: field.dateTime({
|
|
197
|
-
isOptional: true,
|
|
198
|
-
description: "Scheduled end time"
|
|
199
|
-
}),
|
|
200
|
-
startedAt: field.dateTime({
|
|
201
|
-
isOptional: true,
|
|
202
|
-
description: "Actual start time"
|
|
203
|
-
}),
|
|
204
|
-
endedAt: field.dateTime({
|
|
205
|
-
isOptional: true,
|
|
206
|
-
description: "Actual end time"
|
|
207
|
-
}),
|
|
208
|
-
winningVariant: field.string({
|
|
209
|
-
isOptional: true,
|
|
210
|
-
description: "Declared winning variant"
|
|
211
|
-
}),
|
|
212
|
-
results: field.json({
|
|
213
|
-
isOptional: true,
|
|
214
|
-
description: "Experiment results summary"
|
|
215
|
-
}),
|
|
216
|
-
orgId: field.string({
|
|
217
|
-
isOptional: true,
|
|
218
|
-
description: "Organization scope"
|
|
219
|
-
}),
|
|
220
|
-
createdAt: field.createdAt(),
|
|
221
|
-
updatedAt: field.updatedAt(),
|
|
222
|
-
flag: field.belongsTo("FeatureFlag", ["flagId"], ["id"], { onDelete: "Cascade" }),
|
|
223
|
-
assignments: field.hasMany("ExperimentAssignment")
|
|
224
|
-
},
|
|
225
|
-
indexes: [
|
|
226
|
-
index.on(["status"]),
|
|
227
|
-
index.on(["orgId", "status"]),
|
|
228
|
-
index.on(["flagId"])
|
|
229
|
-
],
|
|
230
|
-
enums: [ExperimentStatusEnum]
|
|
231
|
-
});
|
|
232
|
-
/**
|
|
233
|
-
* ExperimentAssignment entity - tracks which variant a subject is assigned to.
|
|
234
|
-
*/
|
|
235
|
-
const ExperimentAssignmentEntity = defineEntity({
|
|
236
|
-
name: "ExperimentAssignment",
|
|
237
|
-
description: "Tracks experiment variant assignments.",
|
|
238
|
-
schema: "lssm_feature_flags",
|
|
239
|
-
map: "experiment_assignment",
|
|
240
|
-
fields: {
|
|
241
|
-
id: field.id({ description: "Unique assignment identifier" }),
|
|
242
|
-
experimentId: field.foreignKey({ description: "Parent experiment" }),
|
|
243
|
-
subjectType: field.string({ description: "Subject type (user, org, session)" }),
|
|
244
|
-
subjectId: field.string({ description: "Subject identifier" }),
|
|
245
|
-
variant: field.string({ description: "Assigned variant key" }),
|
|
246
|
-
bucket: field.int({ description: "Hash bucket (0-99)" }),
|
|
247
|
-
context: field.json({
|
|
248
|
-
isOptional: true,
|
|
249
|
-
description: "Context at assignment time"
|
|
250
|
-
}),
|
|
251
|
-
assignedAt: field.dateTime({ description: "Assignment timestamp" }),
|
|
252
|
-
experiment: field.belongsTo("Experiment", ["experimentId"], ["id"], { onDelete: "Cascade" })
|
|
253
|
-
},
|
|
254
|
-
indexes: [index.unique([
|
|
255
|
-
"experimentId",
|
|
256
|
-
"subjectType",
|
|
257
|
-
"subjectId"
|
|
258
|
-
], { name: "experiment_assignment_unique" }), index.on(["subjectType", "subjectId"])]
|
|
259
|
-
});
|
|
260
|
-
/**
|
|
261
|
-
* FlagEvaluation entity - evaluation log for analytics.
|
|
262
|
-
*/
|
|
263
|
-
const FlagEvaluationEntity = defineEntity({
|
|
264
|
-
name: "FlagEvaluation",
|
|
265
|
-
description: "Log of flag evaluations for debugging and analytics.",
|
|
266
|
-
schema: "lssm_feature_flags",
|
|
267
|
-
map: "flag_evaluation",
|
|
268
|
-
fields: {
|
|
269
|
-
id: field.id({ description: "Unique evaluation identifier" }),
|
|
270
|
-
flagId: field.foreignKey({ description: "Evaluated flag" }),
|
|
271
|
-
flagKey: field.string({ description: "Flag key (denormalized for queries)" }),
|
|
272
|
-
subjectType: field.string({ description: "Subject type (user, org, anonymous)" }),
|
|
273
|
-
subjectId: field.string({ description: "Subject identifier" }),
|
|
274
|
-
result: field.boolean({ description: "Evaluation result" }),
|
|
275
|
-
variant: field.string({
|
|
276
|
-
isOptional: true,
|
|
277
|
-
description: "Served variant (for multivariate)"
|
|
278
|
-
}),
|
|
279
|
-
matchedRuleId: field.string({
|
|
280
|
-
isOptional: true,
|
|
281
|
-
description: "Rule that matched (if any)"
|
|
282
|
-
}),
|
|
283
|
-
reason: field.string({ description: "Evaluation reason (default, rule, experiment, etc.)" }),
|
|
284
|
-
context: field.json({
|
|
285
|
-
isOptional: true,
|
|
286
|
-
description: "Evaluation context"
|
|
287
|
-
}),
|
|
288
|
-
evaluatedAt: field.dateTime({ description: "Evaluation timestamp" }),
|
|
289
|
-
flag: field.belongsTo("FeatureFlag", ["flagId"], ["id"], { onDelete: "Cascade" })
|
|
290
|
-
},
|
|
291
|
-
indexes: [
|
|
292
|
-
index.on(["flagKey", "evaluatedAt"]),
|
|
293
|
-
index.on([
|
|
294
|
-
"subjectType",
|
|
295
|
-
"subjectId",
|
|
296
|
-
"evaluatedAt"
|
|
297
|
-
]),
|
|
298
|
-
index.on(["flagId", "evaluatedAt"])
|
|
299
|
-
]
|
|
300
|
-
});
|
|
301
|
-
/**
|
|
302
|
-
* All feature flag entities for schema composition.
|
|
303
|
-
*/
|
|
304
|
-
const featureFlagEntities = [
|
|
305
|
-
FeatureFlagEntity,
|
|
306
|
-
FlagTargetingRuleEntity,
|
|
307
|
-
ExperimentEntity,
|
|
308
|
-
ExperimentAssignmentEntity,
|
|
309
|
-
FlagEvaluationEntity
|
|
310
|
-
];
|
|
311
|
-
/**
|
|
312
|
-
* Module schema contribution for feature flags.
|
|
313
|
-
*/
|
|
314
|
-
const featureFlagsSchemaContribution = {
|
|
315
|
-
moduleId: "@lssm/lib.feature-flags",
|
|
316
|
-
entities: featureFlagEntities,
|
|
317
|
-
enums: [
|
|
318
|
-
FlagStatusEnum,
|
|
319
|
-
RuleOperatorEnum,
|
|
320
|
-
ExperimentStatusEnum
|
|
321
|
-
]
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
//#endregion
|
|
325
|
-
export { ExperimentAssignmentEntity, ExperimentEntity, ExperimentStatusEnum, FeatureFlagEntity, FlagEvaluationEntity, FlagStatusEnum, FlagTargetingRuleEntity, RuleOperatorEnum, featureFlagEntities, featureFlagsSchemaContribution };
|
|
1
|
+
import{defineEntity as e,defineEntityEnum as t,field as n,index as r}from"@lssm/lib.schema";const i=t({name:`FlagStatus`,values:[`OFF`,`ON`,`GRADUAL`],schema:`lssm_feature_flags`,description:`Status of a feature flag.`}),a=t({name:`RuleOperator`,values:[`EQ`,`NEQ`,`IN`,`NIN`,`CONTAINS`,`NOT_CONTAINS`,`GT`,`GTE`,`LT`,`LTE`,`PERCENTAGE`],schema:`lssm_feature_flags`,description:`Operator for targeting rule conditions.`}),o=t({name:`ExperimentStatus`,values:[`DRAFT`,`RUNNING`,`PAUSED`,`COMPLETED`,`CANCELLED`],schema:`lssm_feature_flags`,description:`Status of an experiment.`}),s=e({name:`FeatureFlag`,description:`A feature flag for controlling feature availability.`,schema:`lssm_feature_flags`,map:`feature_flag`,fields:{id:n.id({description:`Unique flag identifier`}),key:n.string({isUnique:!0,description:`Flag key (e.g., new_dashboard)`}),name:n.string({description:`Human-readable name`}),description:n.string({isOptional:!0,description:`Description of the flag`}),status:n.enum(`FlagStatus`,{default:`OFF`,description:`Flag status`}),defaultValue:n.boolean({default:!1,description:`Default value when no rules match`}),variants:n.json({isOptional:!0,description:`Variant definitions for multivariate flags`}),orgId:n.string({isOptional:!0,description:`Organization scope (null = global)`}),tags:n.json({isOptional:!0,description:`Tags for categorization`}),metadata:n.json({isOptional:!0,description:`Additional metadata`}),createdAt:n.createdAt(),updatedAt:n.updatedAt(),targetingRules:n.hasMany(`FlagTargetingRule`),experiments:n.hasMany(`Experiment`),evaluations:n.hasMany(`FlagEvaluation`)},indexes:[r.on([`orgId`,`key`]),r.on([`status`])],enums:[i]}),c=e({name:`FlagTargetingRule`,description:`A targeting rule for conditional flag evaluation.`,schema:`lssm_feature_flags`,map:`flag_targeting_rule`,fields:{id:n.id({description:`Unique rule identifier`}),flagId:n.foreignKey({description:`Parent feature flag`}),name:n.string({isOptional:!0,description:`Rule name for debugging`}),priority:n.int({default:0,description:`Rule priority (lower = higher priority)`}),enabled:n.boolean({default:!0,description:`Whether rule is active`}),attribute:n.string({description:`Target attribute (userId, orgId, plan, segment, etc.)`}),operator:n.enum(`RuleOperator`,{description:`Comparison operator`}),value:n.json({description:`Target value(s)`}),rolloutPercentage:n.int({isOptional:!0,description:`Percentage for gradual rollout (0-100)`}),serveValue:n.boolean({isOptional:!0,description:`Boolean value to serve`}),serveVariant:n.string({isOptional:!0,description:`Variant key to serve (for multivariate)`}),createdAt:n.createdAt(),updatedAt:n.updatedAt(),flag:n.belongsTo(`FeatureFlag`,[`flagId`],[`id`],{onDelete:`Cascade`})},indexes:[r.on([`flagId`,`priority`]),r.on([`attribute`])],enums:[a]}),l=e({name:`Experiment`,description:`An A/B test experiment.`,schema:`lssm_feature_flags`,map:`experiment`,fields:{id:n.id({description:`Unique experiment identifier`}),key:n.string({isUnique:!0,description:`Experiment key`}),name:n.string({description:`Human-readable name`}),description:n.string({isOptional:!0,description:`Experiment description`}),hypothesis:n.string({isOptional:!0,description:`Experiment hypothesis`}),flagId:n.foreignKey({description:`Associated feature flag`}),status:n.enum(`ExperimentStatus`,{default:`DRAFT`,description:`Experiment status`}),variants:n.json({description:`Variant definitions with split ratios`}),metrics:n.json({isOptional:!0,description:`Metrics to track`}),audiencePercentage:n.int({default:100,description:`Percentage of audience to include`}),audienceFilter:n.json({isOptional:!0,description:`Audience filter criteria`}),scheduledStartAt:n.dateTime({isOptional:!0,description:`Scheduled start time`}),scheduledEndAt:n.dateTime({isOptional:!0,description:`Scheduled end time`}),startedAt:n.dateTime({isOptional:!0,description:`Actual start time`}),endedAt:n.dateTime({isOptional:!0,description:`Actual end time`}),winningVariant:n.string({isOptional:!0,description:`Declared winning variant`}),results:n.json({isOptional:!0,description:`Experiment results summary`}),orgId:n.string({isOptional:!0,description:`Organization scope`}),createdAt:n.createdAt(),updatedAt:n.updatedAt(),flag:n.belongsTo(`FeatureFlag`,[`flagId`],[`id`],{onDelete:`Cascade`}),assignments:n.hasMany(`ExperimentAssignment`)},indexes:[r.on([`status`]),r.on([`orgId`,`status`]),r.on([`flagId`])],enums:[o]}),u=e({name:`ExperimentAssignment`,description:`Tracks experiment variant assignments.`,schema:`lssm_feature_flags`,map:`experiment_assignment`,fields:{id:n.id({description:`Unique assignment identifier`}),experimentId:n.foreignKey({description:`Parent experiment`}),subjectType:n.string({description:`Subject type (user, org, session)`}),subjectId:n.string({description:`Subject identifier`}),variant:n.string({description:`Assigned variant key`}),bucket:n.int({description:`Hash bucket (0-99)`}),context:n.json({isOptional:!0,description:`Context at assignment time`}),assignedAt:n.dateTime({description:`Assignment timestamp`}),experiment:n.belongsTo(`Experiment`,[`experimentId`],[`id`],{onDelete:`Cascade`})},indexes:[r.unique([`experimentId`,`subjectType`,`subjectId`],{name:`experiment_assignment_unique`}),r.on([`subjectType`,`subjectId`])]}),d=e({name:`FlagEvaluation`,description:`Log of flag evaluations for debugging and analytics.`,schema:`lssm_feature_flags`,map:`flag_evaluation`,fields:{id:n.id({description:`Unique evaluation identifier`}),flagId:n.foreignKey({description:`Evaluated flag`}),flagKey:n.string({description:`Flag key (denormalized for queries)`}),subjectType:n.string({description:`Subject type (user, org, anonymous)`}),subjectId:n.string({description:`Subject identifier`}),result:n.boolean({description:`Evaluation result`}),variant:n.string({isOptional:!0,description:`Served variant (for multivariate)`}),matchedRuleId:n.string({isOptional:!0,description:`Rule that matched (if any)`}),reason:n.string({description:`Evaluation reason (default, rule, experiment, etc.)`}),context:n.json({isOptional:!0,description:`Evaluation context`}),evaluatedAt:n.dateTime({description:`Evaluation timestamp`}),flag:n.belongsTo(`FeatureFlag`,[`flagId`],[`id`],{onDelete:`Cascade`})},indexes:[r.on([`flagKey`,`evaluatedAt`]),r.on([`subjectType`,`subjectId`,`evaluatedAt`]),r.on([`flagId`,`evaluatedAt`])]}),f=[s,c,l,u,d],p={moduleId:`@lssm/lib.feature-flags`,entities:f,enums:[i,a,o]};export{u as ExperimentAssignmentEntity,l as ExperimentEntity,o as ExperimentStatusEnum,s as FeatureFlagEntity,d as FlagEvaluationEntity,i as FlagStatusEnum,c as FlagTargetingRuleEntity,a as RuleOperatorEnum,f as featureFlagEntities,p as featureFlagsSchemaContribution};
|
package/dist/evaluation/index.js
CHANGED
|
@@ -1,220 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Simple hash function for consistent bucketing.
|
|
4
|
-
* Uses a deterministic algorithm so the same input always produces the same bucket.
|
|
5
|
-
*/
|
|
6
|
-
function hashToBucket(value, seed = "") {
|
|
7
|
-
const input = `${seed}:${value}`;
|
|
8
|
-
let hash = 0;
|
|
9
|
-
for (let i = 0; i < input.length; i++) {
|
|
10
|
-
const char = input.charCodeAt(i);
|
|
11
|
-
hash = (hash << 5) - hash + char;
|
|
12
|
-
hash = hash & hash;
|
|
13
|
-
}
|
|
14
|
-
return Math.abs(hash % 100);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Get subject identifier from context for consistent hashing.
|
|
18
|
-
*/
|
|
19
|
-
function getSubjectId(context) {
|
|
20
|
-
return context.userId || context.sessionId || context.orgId || "anonymous";
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Evaluate a single targeting rule condition.
|
|
24
|
-
*/
|
|
25
|
-
function evaluateRuleCondition(rule, context) {
|
|
26
|
-
const attributeValue = getAttributeValue(rule.attribute, context);
|
|
27
|
-
switch (rule.operator) {
|
|
28
|
-
case "EQ": return attributeValue === rule.value;
|
|
29
|
-
case "NEQ": return attributeValue !== rule.value;
|
|
30
|
-
case "IN":
|
|
31
|
-
if (!Array.isArray(rule.value)) return false;
|
|
32
|
-
return rule.value.includes(attributeValue);
|
|
33
|
-
case "NIN":
|
|
34
|
-
if (!Array.isArray(rule.value)) return true;
|
|
35
|
-
return !rule.value.includes(attributeValue);
|
|
36
|
-
case "CONTAINS":
|
|
37
|
-
if (typeof attributeValue !== "string" || typeof rule.value !== "string") return false;
|
|
38
|
-
return attributeValue.includes(rule.value);
|
|
39
|
-
case "NOT_CONTAINS":
|
|
40
|
-
if (typeof attributeValue !== "string" || typeof rule.value !== "string") return true;
|
|
41
|
-
return !attributeValue.includes(rule.value);
|
|
42
|
-
case "GT":
|
|
43
|
-
if (typeof attributeValue !== "number" || typeof rule.value !== "number") return false;
|
|
44
|
-
return attributeValue > rule.value;
|
|
45
|
-
case "GTE":
|
|
46
|
-
if (typeof attributeValue !== "number" || typeof rule.value !== "number") return false;
|
|
47
|
-
return attributeValue >= rule.value;
|
|
48
|
-
case "LT":
|
|
49
|
-
if (typeof attributeValue !== "number" || typeof rule.value !== "number") return false;
|
|
50
|
-
return attributeValue < rule.value;
|
|
51
|
-
case "LTE":
|
|
52
|
-
if (typeof attributeValue !== "number" || typeof rule.value !== "number") return false;
|
|
53
|
-
return attributeValue <= rule.value;
|
|
54
|
-
case "PERCENTAGE": return hashToBucket(getSubjectId(context), rule.attribute) < (typeof rule.value === "number" ? rule.value : 0);
|
|
55
|
-
default: return false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Get attribute value from context.
|
|
60
|
-
*/
|
|
61
|
-
function getAttributeValue(attribute, context) {
|
|
62
|
-
switch (attribute) {
|
|
63
|
-
case "userId": return context.userId;
|
|
64
|
-
case "orgId": return context.orgId;
|
|
65
|
-
case "plan": return context.plan;
|
|
66
|
-
case "segment": return context.segment;
|
|
67
|
-
case "sessionId": return context.sessionId;
|
|
68
|
-
default: return context.attributes?.[attribute];
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Feature flag evaluator.
|
|
73
|
-
*
|
|
74
|
-
* Evaluates flags based on:
|
|
75
|
-
* 1. Flag status (OFF/ON/GRADUAL)
|
|
76
|
-
* 2. Targeting rules (in priority order)
|
|
77
|
-
* 3. Experiments (if running)
|
|
78
|
-
* 4. Default value (fallback)
|
|
79
|
-
*/
|
|
80
|
-
var FlagEvaluator = class {
|
|
81
|
-
repository;
|
|
82
|
-
logger;
|
|
83
|
-
logEvaluations;
|
|
84
|
-
constructor(options) {
|
|
85
|
-
this.repository = options.repository;
|
|
86
|
-
this.logger = options.logger;
|
|
87
|
-
this.logEvaluations = options.logEvaluations ?? false;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Evaluate a feature flag.
|
|
91
|
-
*/
|
|
92
|
-
async evaluate(key, context) {
|
|
93
|
-
const orgId = context.orgId;
|
|
94
|
-
const flag = await this.repository.getFlag(key, orgId);
|
|
95
|
-
if (!flag) return this.makeResult(false, "FLAG_NOT_FOUND");
|
|
96
|
-
if (flag.status === "OFF") return this.logAndReturn(flag, context, this.makeResult(false, "FLAG_OFF"));
|
|
97
|
-
if (flag.status === "ON") return this.logAndReturn(flag, context, this.makeResult(true, "FLAG_ON"));
|
|
98
|
-
const sortedRules = [...await this.repository.getRules(flag.id)].filter((r) => r.enabled).sort((a, b) => a.priority - b.priority);
|
|
99
|
-
for (const rule of sortedRules) if (evaluateRuleCondition(rule, context)) {
|
|
100
|
-
if (rule.rolloutPercentage !== void 0 && rule.rolloutPercentage !== null) {
|
|
101
|
-
if (hashToBucket(getSubjectId(context), flag.key) >= rule.rolloutPercentage) continue;
|
|
102
|
-
}
|
|
103
|
-
const enabled = rule.serveValue ?? true;
|
|
104
|
-
return this.logAndReturn(flag, context, this.makeResult(enabled, "RULE_MATCH", rule.serveVariant, rule.id));
|
|
105
|
-
}
|
|
106
|
-
const experiment = await this.repository.getActiveExperiment(flag.id);
|
|
107
|
-
if (experiment && experiment.status === "RUNNING") {
|
|
108
|
-
const result = await this.evaluateExperiment(experiment, context);
|
|
109
|
-
if (result) return this.logAndReturn(flag, context, result);
|
|
110
|
-
}
|
|
111
|
-
return this.logAndReturn(flag, context, this.makeResult(flag.defaultValue, "DEFAULT_VALUE"));
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Evaluate experiment and assign variant.
|
|
115
|
-
*/
|
|
116
|
-
async evaluateExperiment(experiment, context) {
|
|
117
|
-
const subjectId = getSubjectId(context);
|
|
118
|
-
const subjectType = context.userId ? "user" : context.orgId ? "org" : "session";
|
|
119
|
-
if (hashToBucket(subjectId, `${experiment.key}:audience`) >= experiment.audiencePercentage) return null;
|
|
120
|
-
let variant = await this.repository.getExperimentAssignment(experiment.id, subjectType, subjectId);
|
|
121
|
-
if (!variant) {
|
|
122
|
-
const variantBucket = hashToBucket(subjectId, experiment.key);
|
|
123
|
-
variant = this.assignVariant(experiment.variants, variantBucket);
|
|
124
|
-
await this.repository.saveExperimentAssignment(experiment.id, subjectType, subjectId, variant, variantBucket);
|
|
125
|
-
}
|
|
126
|
-
const enabled = variant !== "control";
|
|
127
|
-
return this.makeResult(enabled, "EXPERIMENT_VARIANT", variant, void 0, experiment.id);
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Assign a variant based on bucket and variant percentages.
|
|
131
|
-
*/
|
|
132
|
-
assignVariant(variants, bucket) {
|
|
133
|
-
let cumulative = 0;
|
|
134
|
-
for (const variant of variants) {
|
|
135
|
-
cumulative += variant.percentage;
|
|
136
|
-
if (bucket < cumulative) return variant.key;
|
|
137
|
-
}
|
|
138
|
-
return variants[variants.length - 1]?.key ?? "control";
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Create evaluation result.
|
|
142
|
-
*/
|
|
143
|
-
makeResult(enabled, reason, variant, ruleId, experimentId) {
|
|
144
|
-
return {
|
|
145
|
-
enabled,
|
|
146
|
-
variant,
|
|
147
|
-
reason,
|
|
148
|
-
ruleId,
|
|
149
|
-
experimentId
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Log evaluation and return result.
|
|
154
|
-
*/
|
|
155
|
-
logAndReturn(flag, context, result) {
|
|
156
|
-
if (this.logEvaluations && this.logger) {
|
|
157
|
-
const subjectId = getSubjectId(context);
|
|
158
|
-
const subjectType = context.userId ? "user" : context.orgId ? "org" : "session";
|
|
159
|
-
this.logger.log({
|
|
160
|
-
flagId: flag.id,
|
|
161
|
-
flagKey: flag.key,
|
|
162
|
-
subjectType,
|
|
163
|
-
subjectId,
|
|
164
|
-
result: result.enabled,
|
|
165
|
-
variant: result.variant,
|
|
166
|
-
reason: result.reason,
|
|
167
|
-
ruleId: result.ruleId,
|
|
168
|
-
experimentId: result.experimentId,
|
|
169
|
-
context
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
return result;
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
/**
|
|
176
|
-
* In-memory flag repository for testing and development.
|
|
177
|
-
*/
|
|
178
|
-
var InMemoryFlagRepository = class {
|
|
179
|
-
flags = /* @__PURE__ */ new Map();
|
|
180
|
-
rules = /* @__PURE__ */ new Map();
|
|
181
|
-
experiments = /* @__PURE__ */ new Map();
|
|
182
|
-
assignments = /* @__PURE__ */ new Map();
|
|
183
|
-
addFlag(flag) {
|
|
184
|
-
this.flags.set(flag.key, flag);
|
|
185
|
-
}
|
|
186
|
-
addRule(flagId, rule) {
|
|
187
|
-
const existing = this.rules.get(flagId) || [];
|
|
188
|
-
existing.push(rule);
|
|
189
|
-
this.rules.set(flagId, existing);
|
|
190
|
-
}
|
|
191
|
-
addExperiment(experiment, flagId) {
|
|
192
|
-
this.experiments.set(flagId, experiment);
|
|
193
|
-
}
|
|
194
|
-
async getFlag(key) {
|
|
195
|
-
return this.flags.get(key) || null;
|
|
196
|
-
}
|
|
197
|
-
async getRules(flagId) {
|
|
198
|
-
return this.rules.get(flagId) || [];
|
|
199
|
-
}
|
|
200
|
-
async getActiveExperiment(flagId) {
|
|
201
|
-
return this.experiments.get(flagId) || null;
|
|
202
|
-
}
|
|
203
|
-
async getExperimentAssignment(experimentId, subjectType, subjectId) {
|
|
204
|
-
const key = `${experimentId}:${subjectType}:${subjectId}`;
|
|
205
|
-
return this.assignments.get(key) || null;
|
|
206
|
-
}
|
|
207
|
-
async saveExperimentAssignment(experimentId, subjectType, subjectId, variant) {
|
|
208
|
-
const key = `${experimentId}:${subjectType}:${subjectId}`;
|
|
209
|
-
this.assignments.set(key, variant);
|
|
210
|
-
}
|
|
211
|
-
clear() {
|
|
212
|
-
this.flags.clear();
|
|
213
|
-
this.rules.clear();
|
|
214
|
-
this.experiments.clear();
|
|
215
|
-
this.assignments.clear();
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
//#endregion
|
|
220
|
-
export { FlagEvaluator, InMemoryFlagRepository, evaluateRuleCondition, getSubjectId, hashToBucket };
|
|
1
|
+
function e(e,t=``){let n=`${t}:${e}`,r=0;for(let e=0;e<n.length;e++){let t=n.charCodeAt(e);r=(r<<5)-r+t,r&=r}return Math.abs(r%100)}function t(e){return e.userId||e.sessionId||e.orgId||`anonymous`}function n(n,i){let a=r(n.attribute,i);switch(n.operator){case`EQ`:return a===n.value;case`NEQ`:return a!==n.value;case`IN`:return Array.isArray(n.value)?n.value.includes(a):!1;case`NIN`:return Array.isArray(n.value)?!n.value.includes(a):!0;case`CONTAINS`:return typeof a!=`string`||typeof n.value!=`string`?!1:a.includes(n.value);case`NOT_CONTAINS`:return typeof a!=`string`||typeof n.value!=`string`?!0:!a.includes(n.value);case`GT`:return typeof a!=`number`||typeof n.value!=`number`?!1:a>n.value;case`GTE`:return typeof a!=`number`||typeof n.value!=`number`?!1:a>=n.value;case`LT`:return typeof a!=`number`||typeof n.value!=`number`?!1:a<n.value;case`LTE`:return typeof a!=`number`||typeof n.value!=`number`?!1:a<=n.value;case`PERCENTAGE`:return e(t(i),n.attribute)<(typeof n.value==`number`?n.value:0);default:return!1}}function r(e,t){switch(e){case`userId`:return t.userId;case`orgId`:return t.orgId;case`plan`:return t.plan;case`segment`:return t.segment;case`sessionId`:return t.sessionId;default:return t.attributes?.[e]}}var i=class{repository;logger;logEvaluations;constructor(e){this.repository=e.repository,this.logger=e.logger,this.logEvaluations=e.logEvaluations??!1}async evaluate(r,i){let a=i.orgId,o=await this.repository.getFlag(r,a);if(!o)return this.makeResult(!1,`FLAG_NOT_FOUND`);if(o.status===`OFF`)return this.logAndReturn(o,i,this.makeResult(!1,`FLAG_OFF`));if(o.status===`ON`)return this.logAndReturn(o,i,this.makeResult(!0,`FLAG_ON`));let s=[...await this.repository.getRules(o.id)].filter(e=>e.enabled).sort((e,t)=>e.priority-t.priority);for(let r of s)if(n(r,i)){if(r.rolloutPercentage!==void 0&&r.rolloutPercentage!==null&&e(t(i),o.key)>=r.rolloutPercentage)continue;let n=r.serveValue??!0;return this.logAndReturn(o,i,this.makeResult(n,`RULE_MATCH`,r.serveVariant,r.id))}let c=await this.repository.getActiveExperiment(o.id);if(c&&c.status===`RUNNING`){let e=await this.evaluateExperiment(c,i);if(e)return this.logAndReturn(o,i,e)}return this.logAndReturn(o,i,this.makeResult(o.defaultValue,`DEFAULT_VALUE`))}async evaluateExperiment(n,r){let i=t(r),a=r.userId?`user`:r.orgId?`org`:`session`;if(e(i,`${n.key}:audience`)>=n.audiencePercentage)return null;let o=await this.repository.getExperimentAssignment(n.id,a,i);if(!o){let t=e(i,n.key);o=this.assignVariant(n.variants,t),await this.repository.saveExperimentAssignment(n.id,a,i,o,t)}let s=o!==`control`;return this.makeResult(s,`EXPERIMENT_VARIANT`,o,void 0,n.id)}assignVariant(e,t){let n=0;for(let r of e)if(n+=r.percentage,t<n)return r.key;return e[e.length-1]?.key??`control`}makeResult(e,t,n,r,i){return{enabled:e,variant:n,reason:t,ruleId:r,experimentId:i}}logAndReturn(e,n,r){if(this.logEvaluations&&this.logger){let i=t(n),a=n.userId?`user`:n.orgId?`org`:`session`;this.logger.log({flagId:e.id,flagKey:e.key,subjectType:a,subjectId:i,result:r.enabled,variant:r.variant,reason:r.reason,ruleId:r.ruleId,experimentId:r.experimentId,context:n})}return r}},a=class{flags=new Map;rules=new Map;experiments=new Map;assignments=new Map;addFlag(e){this.flags.set(e.key,e)}addRule(e,t){let n=this.rules.get(e)||[];n.push(t),this.rules.set(e,n)}addExperiment(e,t){this.experiments.set(t,e)}async getFlag(e){return this.flags.get(e)||null}async getRules(e){return this.rules.get(e)||[]}async getActiveExperiment(e){return this.experiments.get(e)||null}async getExperimentAssignment(e,t,n){let r=`${e}:${t}:${n}`;return this.assignments.get(r)||null}async saveExperimentAssignment(e,t,n,r){let i=`${e}:${t}:${n}`;this.assignments.set(i,r)}clear(){this.flags.clear(),this.rules.clear(),this.experiments.clear(),this.assignments.clear()}};export{i as FlagEvaluator,a as InMemoryFlagRepository,n as evaluateRuleCondition,t as getSubjectId,e as hashToBucket};
|