diff --git a/README.md b/README.md
index 67ac9e583..08ab9f5ad 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://coveralls.io/github/optimizely/javascript-sdk)
[](https://choosealicense.com/licenses/apache-2.0/)
-This repository houses the JavaScript SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy). The SDK now features a modular architecture for greater flexibility and control. If you're upgrading from a previous version, see our [Migration Guide](MIGRATION.md).
+This is the official JavaScript and TypeScript SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy). The SDK now features a modular architecture for greater flexibility and control. If you're upgrading from a previous version, see our [Migration Guide](MIGRATION.md).
Optimizely Feature Experimentation is an A/B testing and feature management tool for product development teams that enables you to experiment at every step. Using Optimizely Feature Experimentation allows for every feature on your roadmap to be an opportunity to discover hidden insights. Learn more at [Optimizely.com](https://www.optimizely.com/products/experiment/feature-experimentation/), or see the [developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/introduction).
@@ -16,9 +16,8 @@ Optimizely Rollouts is [free feature flags](https://www.optimizely.com/free-feat
## Get Started
-> For **Browser** applications, refer to the [JavaScript SDK's developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-sdk) for detailed instructions on getting started with using the SDK within client-side applications.
+> Refer to the [JavaScript SDK's developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-sdk) for detailed instructions on getting started with using the SDK.
-> For **Node.js** applications, refer to the [JavaScript (Node) variant of the developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-node-sdk).
> For **Edge Functions**, we provide starter kits that utilize the Optimizely JavaScript SDK for the following platforms:
>
@@ -28,7 +27,7 @@ Optimizely Rollouts is [free feature flags](https://www.optimizely.com/free-feat
> - [Fastly Compute@Edge](https://github.com/optimizely/fastly-compute-starter-kit)
> - [Vercel Edge Middleware](https://github.com/optimizely/vercel-examples/tree/main/edge-middleware/feature-flag-optimizely)
>
-> Note: We recommend using the **Lite** version of the sdk for edge platforms. These starter kits also use the **Lite** variant of the JavaScript SDK which excludes the datafile manager and event processor packages.
+> Note: We recommend using the **Lite** entrypoint (for version < 6) / **Universal** entrypoint (for version >=6) of the sdk for edge platforms. These starter kits also use the **Lite** variant of the JavaScript SDK.
### Prerequisites
@@ -73,7 +72,7 @@ import optimizely from 'npm:@optimizely/optimizely-sdk';
## Use the JavaScript SDK
-See the [Optimizely Feature Experimentation developer documentation for JavaScript](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/javascript-sdk) to learn how to set up your first JavaScript project and use the SDK for client-side applications.
+See the [JavaScript SDK's developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-sdk) to learn how to set up your first JavaScript project using the SDK.
The SDK uses a modular architecture with dedicated components for project configuration, event processing, and more. The examples below demonstrate the recommended initialization pattern.
@@ -121,17 +120,19 @@ optimizelyClient
});
```
-### Initialization (Using HTML)
+### Initialization (Using HTML script tag)
The package has different entry points for different environments. The browser entry point is an ES module, which can be used with an appropriate bundler like **Webpack** or **Rollup**. Additionally, for ease of use during initial evaluations you can include a standalone umd bundle of the SDK in your web page by fetching it from [unpkg](https://unpkg.com/):
```html
-
+
-
+
```
+> ⚠️ **Warning**: Always include a specific version number (such as @6) when using CDN URLs like the `unpkg` example above. If you use a URL without a version, your application may automatically receive breaking changes when a new major version is released, which can lead to unexpected issues.
+
When evaluated, that bundle assigns the SDK's exports to `window.optimizelySdk`. If you wish to use the asset locally (for example, if unpkg is down), you can find it in your local copy of the package at dist/optimizely.browser.umd.min.js. We do not recommend using this method in production settings as it introduces a third-party performance dependency.
As `window.optimizelySdk` should be a global variable at this point, you can continue to use it like so:
@@ -175,21 +176,49 @@ As `window.optimizelySdk` should be a global variable at this point, you can con
```
-Regarding `EventDispatcher`s: In Node.js environment, the default `EventDispatcher` is powered by the [`http/s`](https://nodejs.org/api/http.html) module.
+### Closing the SDK Instance
+
+Depending on the sdk configuration, the client instance might schedule tasks in the background. If the instance has background tasks scheduled,
+then the instance will not be garbage collected even though there are no more references to the instance in the code. (Basically, the background tasks will still hold references to the instance). Therefore, it's important to close it to properly clean up resources.
+
+```javascript
+// Close the Optimizely client when you're done using it
+optimizelyClient.close()
+```
+Using the following settings will cause background tasks to be scheduled
+
+- Polling Datafile Manager
+- Batch Event Processor with batchSize > 1
+- ODP manager with eventBatchSize > 1
+
+
+
+> ⚠️ **Warning**: Failure to close SDK instances when they're no longer needed may result in memory leaks. This is particularly important for applications that create multiple instances over time. For some environment like SSR applications, it might not be convenient to close each instance, in which case, the `disposable` option of `createInstance` can be used to disable all background tasks on the server side, allowing the instance to be garbage collected.
+
+
+## Special Notes
+
+### Migration Guides
+
+If you're updating your SDK version, please check the appropriate migration guide:
+
+- **Migrating from 5.x or lower to 6.x**: See our [Migration Guide](MIGRATION.md) for detailed instructions on updating to the new modular architecture.
+- **Migrating from 4.x or lower to 5.x**: Please refer to the [Changelog](CHANGELOG.md#500---january-19-2024) for details on these breaking changes.
## SDK Development
### Unit Tests
-There is a mix of testing paradigms used within the JavaScript SDK which include Mocha, Chai, Karma, and Jest, indicated by their respective `*.tests.js` and `*.spec.ts` filenames.
+There is a mix of testing paradigms used within the JavaScript SDK which include Mocha, Chai, Karma, and Vitest, indicated by their respective `*.tests.js` and `*.spec.ts` filenames.
When contributing code to the SDK, aim to keep the percentage of code test coverage at the current level ([](https://coveralls.io/github/optimizely/javascript-sdk)) or above.
-To run unit tests on the primary JavaScript SDK package source code, you can take the following steps:
+To run unit tests, you can take the following steps:
-1. On your command line or terminal, navigate to the `~/javascript-sdk/packages/optimizely-sdk` directory.
-2. Ensure that you have run `npm install` to install all project dependencies.
-3. Run `npm test` to run all test files.
+1. Ensure that you have run `npm install` to install all project dependencies.
+2. Run `npm test` to run all test files.
+3. Run `npm run test-vitest` to run only tests written using Vitest.
+4. Run `npm run test-mocha` to run only tests written using Mocha.
4. (For cross-browser testing) Run `npm run test-xbrowser` to run tests in many browsers via BrowserStack.
5. Resolve any tests that fail before continuing with your contribution.
@@ -215,14 +244,6 @@ npm run test-xbrowser
For more information regarding contributing to the Optimizely JavaScript SDK, please read [Contributing](CONTRIBUTING.md).
-## Special Notes
-
-### Migration Guides
-
-If you're updating your SDK version, please check the appropriate migration guide:
-
-- **Migrating from 5.x to 6.x**: See our [Migration Guide](MIGRATION.md) for detailed instructions on updating to the new modular architecture.
-- **Migrating from 4.x to 5.x**: Please refer to the [Changelog](CHANGELOG.md#500---january-19-2024) for details on these breaking changes.
### Feature Management access
@@ -232,7 +253,7 @@ To access the Feature Management configuration in the Optimizely dashboard, plea
`@optimizely/optimizely-sdk` is developed and maintained by [Optimizely](https://optimizely.com) and many [contributors](https://github.com/optimizely/javascript-sdk/graphs/contributors). If you're interested in learning more about what Optimizely Feature Experimentation can do for your company you can visit the [official Optimizely Feature Experimentation product page here](https://www.optimizely.com/products/experiment/feature-experimentation/) to learn more.
-First-party code (under `packages/optimizely-sdk/lib/`, `packages/datafile-manager/lib`, `packages/datafile-manager/src`, `packages/datafile-manager/__test__`, `packages/event-processor/src`, `packages/event-processor/__tests__`, `packages/logging/src`, `packages/logging/__tests__`, `packages/utils/src`, `packages/utils/__tests__`) is copyright Optimizely, Inc. and contributors, licensed under Apache 2.0.
+First-party code (under `lib/`) is copyright Optimizely, Inc., licensed under Apache 2.0.
### Other Optimizely SDKs
diff --git a/lib/core/bucketer/index.spec.ts b/lib/core/bucketer/index.spec.ts
index b3aac5158..942295356 100644
--- a/lib/core/bucketer/index.spec.ts
+++ b/lib/core/bucketer/index.spec.ts
@@ -80,6 +80,7 @@ describe('excluding groups', () => {
experimentIdMap: configObj.experimentIdMap,
groupIdMap: configObj.groupIdMap,
logger: mockLogger,
+ validateEntity: true,
};
vi.spyOn(bucketValueGenerator, 'generateBucketValue')
@@ -127,6 +128,7 @@ describe('including groups: random', () => {
groupIdMap: configObj.groupIdMap,
logger: mockLogger,
userId: 'testUser',
+ validateEntity: true,
};
});
@@ -228,6 +230,7 @@ describe('including groups: overlapping', () => {
groupIdMap: configObj.groupIdMap,
logger: mockLogger,
userId: 'testUser',
+ validateEntity: true,
};
});
@@ -280,6 +283,7 @@ describe('bucket value falls into empty traffic allocation ranges', () => {
experimentIdMap: configObj.experimentIdMap,
groupIdMap: configObj.groupIdMap,
logger: mockLogger,
+ validateEntity: true,
};
});
@@ -329,6 +333,7 @@ describe('traffic allocation has invalid variation ids', () => {
experimentIdMap: configObj.experimentIdMap,
groupIdMap: configObj.groupIdMap,
logger: mockLogger,
+ validateEntity: true,
};
});
@@ -359,6 +364,7 @@ describe('testBucketWithBucketingId', () => {
variationIdMap: configObj.variationIdMap,
experimentIdMap: configObj.experimentIdMap,
groupIdMap: configObj.groupIdMap,
+ validateEntity: true,
};
});
diff --git a/lib/core/bucketer/index.tests.js b/lib/core/bucketer/index.tests.js
index 0bdf62f4a..a1e046088 100644
--- a/lib/core/bucketer/index.tests.js
+++ b/lib/core/bucketer/index.tests.js
@@ -74,6 +74,7 @@ describe('lib/core/bucketer', function () {
experimentIdMap: configObj.experimentIdMap,
groupIdMap: configObj.groupIdMap,
logger: createdLogger,
+ validateEntity: true,
};
sinon
.stub(bucketValueGenerator, 'generateBucketValue')
@@ -115,6 +116,7 @@ describe('lib/core/bucketer', function () {
experimentIdMap: configObj.experimentIdMap,
groupIdMap: configObj.groupIdMap,
logger: createdLogger,
+ validateEntity: true,
};
bucketerStub = sinon.stub(bucketValueGenerator, 'generateBucketValue');
});
@@ -135,6 +137,7 @@ describe('lib/core/bucketer', function () {
groupIdMap: configObj.groupIdMap,
logger: createdLogger,
userId: 'testUser',
+ validateEntity: true,
};
});
@@ -225,6 +228,7 @@ describe('lib/core/bucketer', function () {
groupIdMap: configObj.groupIdMap,
logger: createdLogger,
userId: 'testUser',
+ validateEntity: true,
};
});
@@ -269,6 +273,7 @@ describe('lib/core/bucketer', function () {
experimentIdMap: configObj.experimentIdMap,
groupIdMap: configObj.groupIdMap,
logger: createdLogger,
+ validateEntity: true,
};
});
@@ -316,6 +321,7 @@ describe('lib/core/bucketer', function () {
experimentIdMap: configObj.experimentIdMap,
groupIdMap: configObj.groupIdMap,
logger: createdLogger,
+ validateEntity: true,
};
});
@@ -365,6 +371,7 @@ describe('lib/core/bucketer', function () {
experimentIdMap: configObj.experimentIdMap,
groupIdMap: configObj.groupIdMap,
logger: createdLogger,
+ validateEntity: true,
};
});
diff --git a/lib/core/bucketer/index.ts b/lib/core/bucketer/index.ts
index 686f49abd..b5a5e58c6 100644
--- a/lib/core/bucketer/index.ts
+++ b/lib/core/bucketer/index.ts
@@ -138,17 +138,16 @@ export const bucket = function(bucketerParams: BucketerParams): DecisionResponse
]);
const entityId = _findBucket(bucketValue, bucketerParams.trafficAllocationConfig);
- if (entityId !== null) {
- if (!bucketerParams.variationIdMap[entityId]) {
- if (entityId) {
- bucketerParams.logger?.warn(INVALID_VARIATION_ID);
- decideReasons.push([INVALID_VARIATION_ID]);
- }
- return {
- result: null,
- reasons: decideReasons,
- };
+
+ if (bucketerParams.validateEntity && entityId !== null && !bucketerParams.variationIdMap[entityId]) {
+ if (entityId) {
+ bucketerParams.logger?.warn(INVALID_VARIATION_ID);
+ decideReasons.push([INVALID_VARIATION_ID]);
}
+ return {
+ result: null,
+ reasons: decideReasons,
+ };
}
return {
diff --git a/lib/core/decision_service/index.tests.js b/lib/core/decision_service/index.tests.js
index cdc4dc7c7..346814857 100644
--- a/lib/core/decision_service/index.tests.js
+++ b/lib/core/decision_service/index.tests.js
@@ -653,6 +653,7 @@ describe('lib/core/decision_service', function() {
experimentIdMap: configObj.experimentIdMap,
experimentKeyMap: configObj.experimentKeyMap,
groupIdMap: configObj.groupIdMap,
+ validateEntity: true,
};
assert.deepEqual(bucketerParams, expectedParams);
diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts
index 5d5e57da9..70673d68e 100644
--- a/lib/core/decision_service/index.ts
+++ b/lib/core/decision_service/index.ts
@@ -594,12 +594,16 @@ export class DecisionService {
bucketingId: string,
userId: string
): BucketerParams {
+ let validateEntity = true;
+
let trafficAllocationConfig: TrafficAllocation[] = getTrafficAllocation(configObj, experiment.id);
if (experiment.cmab) {
trafficAllocationConfig = [{
entityId: CMAB_DUMMY_ENTITY_ID,
endOfRange: experiment.cmab.trafficAllocation
}];
+
+ validateEntity = false;
}
return {
@@ -613,6 +617,7 @@ export class DecisionService {
trafficAllocationConfig,
userId,
variationIdMap: configObj.variationIdMap,
+ validateEntity,
}
}
diff --git a/lib/event_processor/batch_event_processor.ts b/lib/event_processor/batch_event_processor.ts
index b573ca6aa..6ad19eaf8 100644
--- a/lib/event_processor/batch_event_processor.ts
+++ b/lib/event_processor/batch_event_processor.ts
@@ -266,6 +266,10 @@ export class BatchEventProcessor extends BaseService implements EventProcessor {
}
private async readEventCountInStore(store: Store): Promise {
+ if (this.eventCountInStore !== undefined) {
+ return;
+ }
+
try {
const keys = await store.getKeys();
this.eventCountInStore = keys.length;
diff --git a/lib/event_processor/event_builder/log_event.spec.ts b/lib/event_processor/event_builder/log_event.spec.ts
index 54a9c2acf..ad3b22b94 100644
--- a/lib/event_processor/event_builder/log_event.spec.ts
+++ b/lib/event_processor/event_builder/log_event.spec.ts
@@ -16,15 +16,16 @@
import { describe, it, expect } from 'vitest';
import {
- buildConversionEventV1,
- buildImpressionEventV1,
makeEventBatch,
+ buildLogEvent,
} from './log_event';
-import { ImpressionEvent, ConversionEvent } from './user_event';
+import { ImpressionEvent, ConversionEvent, UserEvent } from './user_event';
+import { Region } from '../../project_config/project_config';
-describe('buildImpressionEventV1', () => {
- it('should build an ImpressionEventV1 when experiment and variation are defined', () => {
+
+describe('makeEventBatch', () => {
+ it('should build a batch with single impression event when experiment and variation are defined', () => {
const impressionEvent: ImpressionEvent = {
type: 'impression',
timestamp: 69,
@@ -65,7 +66,7 @@ describe('buildImpressionEventV1', () => {
enabled: true,
}
- const result = buildImpressionEventV1(impressionEvent)
+ const result = makeEventBatch([impressionEvent])
expect(result).toEqual({
client_name: 'node-sdk',
client_version: '3.0.0',
@@ -123,7 +124,7 @@ describe('buildImpressionEventV1', () => {
})
})
- it('should build an ImpressionEventV1 when experiment and variation are not defined', () => {
+ it('should build a batch with simlge impression event when experiment and variation are not defined', () => {
const impressionEvent: ImpressionEvent = {
type: 'impression',
timestamp: 69,
@@ -164,7 +165,7 @@ describe('buildImpressionEventV1', () => {
enabled: true,
}
- const result = buildImpressionEventV1(impressionEvent)
+ const result = makeEventBatch([impressionEvent])
expect(result).toEqual({
client_name: 'node-sdk',
client_version: '3.0.0',
@@ -220,11 +221,9 @@ describe('buildImpressionEventV1', () => {
},
],
})
- })
-})
+ });
-describe('buildConversionEventV1', () => {
- it('should build a ConversionEventV1 when tags object is defined', () => {
+ it('should build a batch with single conversion event when tags object is defined', () => {
const conversionEvent: ConversionEvent = {
type: 'conversion',
timestamp: 69,
@@ -260,7 +259,7 @@ describe('buildConversionEventV1', () => {
value: 123,
}
- const result = buildConversionEventV1(conversionEvent)
+ const result = makeEventBatch([conversionEvent])
expect(result).toEqual({
client_name: 'node-sdk',
client_version: '3.0.0',
@@ -311,7 +310,7 @@ describe('buildConversionEventV1', () => {
})
})
- it('should build a ConversionEventV1 when tags object is undefined', () => {
+ it('should build a batch with single conversion event when when tags object is undefined', () => {
const conversionEvent: ConversionEvent = {
type: 'conversion',
timestamp: 69,
@@ -343,7 +342,7 @@ describe('buildConversionEventV1', () => {
value: 123,
}
- const result = buildConversionEventV1(conversionEvent)
+ const result = makeEventBatch([conversionEvent])
expect(result).toEqual({
client_name: 'node-sdk',
client_version: '3.0.0',
@@ -390,7 +389,7 @@ describe('buildConversionEventV1', () => {
})
})
- it('should build a ConversionEventV1 when event id is null', () => {
+ it('should build a batch with single conversion event when event id is null', () => {
const conversionEvent: ConversionEvent = {
type: 'conversion',
timestamp: 69,
@@ -422,7 +421,7 @@ describe('buildConversionEventV1', () => {
value: 123,
}
- const result = buildConversionEventV1(conversionEvent)
+ const result = makeEventBatch([conversionEvent])
expect(result).toEqual({
client_name: 'node-sdk',
client_version: '3.0.0',
@@ -469,7 +468,7 @@ describe('buildConversionEventV1', () => {
})
})
- it('should include revenue and value if they are 0', () => {
+ it('should include revenue and value for conversion events if they are 0', () => {
const conversionEvent: ConversionEvent = {
type: 'conversion',
timestamp: 69,
@@ -505,7 +504,7 @@ describe('buildConversionEventV1', () => {
value: 0,
}
- const result = buildConversionEventV1(conversionEvent)
+ const result = makeEventBatch([conversionEvent])
expect(result).toEqual({
client_name: 'node-sdk',
client_version: '3.0.0',
@@ -591,7 +590,7 @@ describe('buildConversionEventV1', () => {
value: 123,
}
- const result = buildConversionEventV1(conversionEvent)
+ const result = makeEventBatch([conversionEvent])
expect(result).toEqual({
client_name: 'node-sdk',
client_version: '3.0.0',
@@ -635,9 +634,7 @@ describe('buildConversionEventV1', () => {
],
})
})
-})
-describe('makeEventBatch', () => {
it('should batch Conversion and Impression events together', () => {
const conversionEvent: ConversionEvent = {
type: 'conversion',
@@ -810,3 +807,64 @@ describe('makeEventBatch', () => {
})
})
+describe('buildLogEvent', () => {
+ it('should select the correct URL based on the event context region', () => {
+ const baseEvent: ImpressionEvent = {
+ type: 'impression',
+ timestamp: 69,
+ uuid: 'uuid',
+ context: {
+ accountId: 'accountId',
+ projectId: 'projectId',
+ clientName: 'node-sdk',
+ clientVersion: '3.0.0',
+ revision: 'revision',
+ botFiltering: true,
+ anonymizeIP: true
+ },
+ user: {
+ id: 'userId',
+ attributes: []
+ },
+ layer: {
+ id: 'layerId'
+ },
+ experiment: {
+ id: 'expId',
+ key: 'expKey'
+ },
+ variation: {
+ id: 'varId',
+ key: 'varKey'
+ },
+ ruleKey: 'expKey',
+ flagKey: 'flagKey1',
+ ruleType: 'experiment',
+ enabled: true
+ };
+
+ // Test for US region
+ const usEvent = {
+ ...baseEvent,
+ context: {
+ ...baseEvent.context,
+ region: 'US' as Region
+ }
+ };
+
+ const usResult = buildLogEvent([usEvent]);
+ expect(usResult.url).toBe('https://logx.optimizely.com/v1/events');
+
+ // Test for EU region
+ const euEvent = {
+ ...baseEvent,
+ context: {
+ ...baseEvent.context,
+ region: 'EU' as Region
+ }
+ };
+
+ const euResult = buildLogEvent([euEvent]);
+ expect(euResult.url).toBe('https://eu.logx.optimizely.com/v1/events');
+ });
+});
diff --git a/lib/event_processor/event_builder/log_event.ts b/lib/event_processor/event_builder/log_event.ts
index 8e65d6ba1..d3ec940fa 100644
--- a/lib/event_processor/event_builder/log_event.ts
+++ b/lib/event_processor/event_builder/log_event.ts
@@ -19,10 +19,16 @@ import { CONTROL_ATTRIBUTES } from '../../utils/enums';
import { LogEvent } from '../event_dispatcher/event_dispatcher';
import { EventTags } from '../../shared_types';
+import { Region } from '../../project_config/project_config';
const ACTIVATE_EVENT_KEY = 'campaign_activated'
const CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom'
+export const logxEndpoint: Record = {
+ US: 'https://logx.optimizely.com/v1/events',
+ EU: 'https://eu.logx.optimizely.com/v1/events',
+}
+
export type EventBatch = {
account_id: string
project_id: string
@@ -214,51 +220,12 @@ function makeVisitor(data: ImpressionEvent | ConversionEvent): Visitor {
return visitor
}
-/**
- * Event for usage with v1 logtier
- *
- * @export
- * @interface EventBuilderV1
- */
-export function buildImpressionEventV1(data: ImpressionEvent): EventBatch {
- const visitor = makeVisitor(data)
- visitor.snapshots.push(makeDecisionSnapshot(data))
-
- return {
- client_name: data.context.clientName,
- client_version: data.context.clientVersion,
-
- account_id: data.context.accountId,
- project_id: data.context.projectId,
- revision: data.context.revision,
- anonymize_ip: data.context.anonymizeIP,
- enrich_decisions: true,
-
- visitors: [visitor],
- }
-}
-
-export function buildConversionEventV1(data: ConversionEvent): EventBatch {
- const visitor = makeVisitor(data)
- visitor.snapshots.push(makeConversionSnapshot(data))
-
- return {
- client_name: data.context.clientName,
- client_version: data.context.clientVersion,
-
- account_id: data.context.accountId,
- project_id: data.context.projectId,
- revision: data.context.revision,
- anonymize_ip: data.context.anonymizeIP,
- enrich_decisions: true,
-
- visitors: [visitor],
- }
-}
-
export function buildLogEvent(events: UserEvent[]): LogEvent {
+ const region = events[0]?.context.region || 'US';
+ const url = logxEndpoint[region];
+
return {
- url: 'https://logx.optimizely.com/v1/events',
+ url,
httpVerb: 'POST',
params: makeEventBatch(events),
}
diff --git a/lib/event_processor/event_builder/user_event.spec.ts b/lib/event_processor/event_builder/user_event.spec.ts
new file mode 100644
index 000000000..e8cb373b3
--- /dev/null
+++ b/lib/event_processor/event_builder/user_event.spec.ts
@@ -0,0 +1,81 @@
+import { describe, it, expect, vi } from 'vitest';
+import { buildImpressionEvent, buildConversionEvent } from './user_event';
+import { createProjectConfig, ProjectConfig } from '../../project_config/project_config';
+import { DecisionObj } from '../../core/decision_service';
+import testData from '../../tests/test_data';
+
+describe('buildImpressionEvent', () => {
+ it('should use correct region from projectConfig in event context', () => {
+ const projectConfig = createProjectConfig(
+ testData.getTestProjectConfig(),
+ )
+
+ const experiment = projectConfig.experiments[0];
+ const variation = experiment.variations[0];
+
+ const decisionObj = {
+ experiment,
+ variation,
+ decisionSource: 'experiment',
+ } as DecisionObj;
+
+
+ const impressionEvent = buildImpressionEvent({
+ configObj: projectConfig,
+ decisionObj,
+ userId: 'test_user',
+ flagKey: 'test_flag',
+ enabled: true,
+ clientEngine: 'node-sdk',
+ clientVersion: '1.0.0',
+ });
+
+ expect(impressionEvent.context.region).toBe('US');
+
+ projectConfig.region = 'EU';
+
+ const impressionEventEU = buildImpressionEvent({
+ configObj: projectConfig,
+ decisionObj,
+ userId: 'test_user',
+ flagKey: 'test_flag',
+ enabled: true,
+ clientEngine: 'node-sdk',
+ clientVersion: '1.0.0',
+ });
+
+ expect(impressionEventEU.context.region).toBe('EU');
+ });
+});
+
+describe('buildConversionEvent', () => {
+ it('should use correct region from projectConfig in event context', () => {
+ const projectConfig = createProjectConfig(
+ testData.getTestProjectConfig(),
+ )
+
+ const conversionEvent = buildConversionEvent({
+ configObj: projectConfig,
+ userId: 'test_user',
+ eventKey: 'test_event',
+ eventTags: { revenue: 1000 },
+ clientEngine: 'node-sdk',
+ clientVersion: '1.0.0',
+ });
+
+ expect(conversionEvent.context.region).toBe('US');
+
+ projectConfig.region = 'EU';
+
+ const conversionEventEU = buildConversionEvent({
+ configObj: projectConfig,
+ userId: 'test_user',
+ eventKey: 'test_event',
+ eventTags: { revenue: 1000 },
+ clientEngine: 'node-sdk',
+ clientVersion: '1.0.0',
+ });
+
+ expect(conversionEventEU.context.region).toBe('EU');
+ });
+});
diff --git a/lib/event_processor/event_builder/user_event.tests.js b/lib/event_processor/event_builder/user_event.tests.js
index 19964e931..30f271d0e 100644
--- a/lib/event_processor/event_builder/user_event.tests.js
+++ b/lib/event_processor/event_builder/user_event.tests.js
@@ -26,6 +26,7 @@ describe('user_event', function() {
beforeEach(function() {
configObj = {
+ region: 'US',
accountId: 'accountId',
projectId: 'projectId',
revision: '69',
@@ -106,6 +107,7 @@ describe('user_event', function() {
timestamp: 100,
uuid: 'uuid',
context: {
+ region: 'US',
accountId: 'accountId',
projectId: 'projectId',
revision: '69',
@@ -200,6 +202,7 @@ describe('user_event', function() {
timestamp: 100,
uuid: 'uuid',
context: {
+ region: 'US',
accountId: 'accountId',
projectId: 'projectId',
revision: '69',
@@ -270,6 +273,7 @@ describe('user_event', function() {
timestamp: 100,
uuid: 'uuid',
context: {
+ region: 'US',
accountId: 'accountId',
projectId: 'projectId',
revision: '69',
@@ -336,6 +340,7 @@ describe('user_event', function() {
timestamp: 100,
uuid: 'uuid',
context: {
+ region: 'US',
accountId: 'accountId',
projectId: 'projectId',
revision: '69',
diff --git a/lib/event_processor/event_builder/user_event.ts b/lib/event_processor/event_builder/user_event.ts
index c6c6c5446..5d098e87b 100644
--- a/lib/event_processor/event_builder/user_event.ts
+++ b/lib/event_processor/event_builder/user_event.ts
@@ -23,6 +23,7 @@ import {
getEventId,
getLayerId,
ProjectConfig,
+ Region,
} from '../../project_config/project_config';
import { EventTags, UserAttributes } from '../../shared_types';
@@ -35,6 +36,7 @@ export type VisitorAttribute = {
}
type EventContext = {
+ region?: Region;
accountId: string;
projectId: string;
revision: string;
@@ -44,8 +46,11 @@ type EventContext = {
botFiltering?: boolean;
}
-export type BaseUserEvent = {
- type: 'impression' | 'conversion';
+type EventType = 'impression' | 'conversion';
+
+
+export type BaseUserEvent = {
+ type: T;
timestamp: number;
uuid: string;
context: EventContext;
@@ -55,9 +60,7 @@ export type BaseUserEvent = {
};
};
-export type ImpressionEvent = BaseUserEvent & {
- type: 'impression';
-
+export type ImpressionEvent = BaseUserEvent<'impression'> & {
layer: {
id: string | null;
} | null;
@@ -79,9 +82,7 @@ export type ImpressionEvent = BaseUserEvent & {
cmabUuid?: string;
};
-export type ConversionEvent = BaseUserEvent & {
- type: 'conversion';
-
+export type ConversionEvent = BaseUserEvent<'conversion'> & {
event: {
id: string | null;
key: string;
@@ -97,7 +98,12 @@ export type UserEvent = ImpressionEvent | ConversionEvent;
export const areEventContextsEqual = (eventA: UserEvent, eventB: UserEvent): boolean => {
const contextA = eventA.context
const contextB = eventB.context
+
+ const regionA: Region = contextA.region || 'US';
+ const regionB: Region = contextB.region || 'US';
+
return (
+ regionA === regionB &&
contextA.accountId === contextB.accountId &&
contextA.projectId === contextB.projectId &&
contextA.clientName === contextB.clientName &&
@@ -108,6 +114,43 @@ export const areEventContextsEqual = (eventA: UserEvent, eventB: UserEvent): boo
)
}
+const buildBaseEvent = ({
+ configObj,
+ userId,
+ userAttributes,
+ clientEngine,
+ clientVersion,
+ type,
+}: {
+ configObj: ProjectConfig;
+ userId: string;
+ userAttributes?: UserAttributes;
+ clientEngine: string;
+ clientVersion: string;
+ type: T;
+}): BaseUserEvent => {
+ return {
+ type,
+ timestamp: fns.currentTimestamp(),
+ uuid: fns.uuid(),
+ context: {
+ region: configObj.region,
+ accountId: configObj.accountId,
+ projectId: configObj.projectId,
+ revision: configObj.revision,
+ clientName: clientEngine,
+ clientVersion: clientVersion,
+ anonymizeIP: configObj.anonymizeIP || false,
+ botFiltering: configObj.botFiltering,
+ },
+ user: {
+ id: userId,
+ attributes: buildVisitorAttributes(configObj, userAttributes),
+ },
+ };
+
+}
+
export type ImpressionConfig = {
decisionObj: DecisionObj;
userId: string;
@@ -119,7 +162,6 @@ export type ImpressionConfig = {
configObj: ProjectConfig;
}
-
/**
* Creates an ImpressionEvent object from decision data
* @param {ImpressionConfig} config
@@ -146,24 +188,14 @@ export const buildImpressionEvent = function({
const layerId = experimentId !== null ? getLayerId(configObj, experimentId) : null;
return {
- type: 'impression',
- timestamp: fns.currentTimestamp(),
- uuid: fns.uuid(),
-
- user: {
- id: userId,
- attributes: buildVisitorAttributes(configObj, userAttributes),
- },
-
- context: {
- accountId: configObj.accountId,
- projectId: configObj.projectId,
- revision: configObj.revision,
- clientName: clientEngine,
- clientVersion: clientVersion,
- anonymizeIP: configObj.anonymizeIP || false,
- botFiltering: configObj.botFiltering,
- },
+ ...buildBaseEvent({
+ configObj,
+ userId,
+ userAttributes,
+ clientEngine,
+ clientVersion,
+ type: 'impression',
+ }),
layer: {
id: layerId,
@@ -218,24 +250,14 @@ export const buildConversionEvent = function({
const eventValue = eventTags ? eventTagUtils.getEventValue(eventTags, logger) : null;
return {
- type: 'conversion',
- timestamp: fns.currentTimestamp(),
- uuid: fns.uuid(),
-
- user: {
- id: userId,
- attributes: buildVisitorAttributes(configObj, userAttributes),
- },
-
- context: {
- accountId: configObj.accountId,
- projectId: configObj.projectId,
- revision: configObj.revision,
- clientName: clientEngine,
- clientVersion: clientVersion,
- anonymizeIP: configObj.anonymizeIP || false,
- botFiltering: configObj.botFiltering,
- },
+ ...buildBaseEvent({
+ configObj,
+ userId,
+ userAttributes,
+ clientEngine,
+ clientVersion,
+ type: 'conversion',
+ }),
event: {
id: eventId,
diff --git a/lib/feature_toggle.ts b/lib/feature_toggle.ts
new file mode 100644
index 000000000..22254e4f0
--- /dev/null
+++ b/lib/feature_toggle.ts
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2025, Optimizely
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+
+/**
+ * This module contains feature flags that control the availability of features under development.
+ * Each flag represents a feature that is not yet ready for production release. These flags
+ * serve multiple purposes in our development workflow:
+ *
+ * When a new feature is in development, it can be safely merged into the main branch
+ * while remaining disabled in production. This allows continuous integration without
+ * affecting the stability of production releases. The feature code will be automatically
+ * removed in production builds through tree-shaking when the flag is disabled.
+ *
+ * During development and testing, these flags can be easily mocked to enable/disable
+ * specific features. Once a feature is complete and ready for release, its corresponding
+ * flag and all associated checks can be removed from the codebase.
+ */
+
+export const holdout = () => false;
diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts
index 5a0259ee4..662488914 100644
--- a/lib/project_config/project_config.spec.ts
+++ b/lib/project_config/project_config.spec.ts
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { describe, it, expect, beforeEach, afterEach, vi, assert, Mock } from 'vitest';
+import { describe, it, expect, beforeEach, afterEach, vi, assert, Mock, beforeAll, afterAll } from 'vitest';
import { sprintf } from '../utils/fns';
import { keyBy } from '../utils/fns';
-import projectConfig, { ProjectConfig } from './project_config';
+import projectConfig, { ProjectConfig, getHoldoutsForFlag } from './project_config';
import { FEATURE_VARIABLE_TYPES, LOG_LEVEL } from '../utils/enums';
import testDatafile from '../tests/test_data';
import configValidator from '../utils/config_validator';
@@ -32,14 +32,44 @@ import {
import { getMockLogger } from '../tests/mock/mock_logger';
import { VariableType } from '../shared_types';
import { OptimizelyError } from '../error/optimizly_error';
+import { mock } from 'node:test';
const buildLogMessageFromArgs = (args: any[]) => sprintf(args[1], ...args.splice(2));
const cloneDeep = (obj: any) => JSON.parse(JSON.stringify(obj));
const logger = getMockLogger();
+const mockHoldoutToggle = vi.hoisted(() => vi.fn());
+
+vi.mock('../feature_toggle', () => {
+ return {
+ holdout: mockHoldoutToggle,
+ };
+});
+
describe('createProjectConfig', () => {
let configObj: ProjectConfig;
+ it('should use US region when no region is specified in datafile', () => {
+ const datafile = testDatafile.getTestProjectConfig();
+ const config = projectConfig.createProjectConfig(datafile);
+
+ expect(config.region).toBe('US');
+ });
+
+ it('should parse region specified in datafile correctly', () => {
+ const datafileUs = testDatafile.getTestProjectConfig();
+ datafileUs.region = 'US';
+
+ const configUs = projectConfig.createProjectConfig(datafileUs);
+ expect(configUs.region).toBe('US');
+
+ const datafileEu = testDatafile.getTestProjectConfig();
+ datafileEu.region = 'EU';
+ const configEu = projectConfig.createProjectConfig(datafileEu);
+
+ expect(configEu.region).toBe('EU');
+ });
+
it('should set properties correctly when createProjectConfig is called', () => {
const testData: Record = testDatafile.getTestProjectConfig();
configObj = projectConfig.createProjectConfig(testData as JSON);
@@ -277,6 +307,191 @@ describe('createProjectConfig - cmab experiments', () => {
});
});
+const getHoldoutDatafile = () => {
+ const datafile = testDatafile.getTestDecideProjectConfig();
+
+ // Add holdouts to the datafile
+ datafile.holdouts = [
+ {
+ id: 'holdout_id_1',
+ key: 'holdout_1',
+ status: 'Running',
+ includeFlags: [],
+ excludeFlags: [],
+ audienceIds: ['13389130056'],
+ audienceConditions: ['or', '13389130056'],
+ variations: [
+ {
+ id: 'var_id_1',
+ key: 'holdout_variation_1',
+ variables: []
+ }
+ ],
+ trafficAllocation: [
+ {
+ entityId: 'var_id_1',
+ endOfRange: 5000
+ }
+ ]
+ },
+ {
+ id: 'holdout_id_2',
+ key: 'holdout_2',
+ status: 'Running',
+ includeFlags: [],
+ excludeFlags: ['feature_3'],
+ audienceIds: [],
+ audienceConditions: [],
+ variations: [
+ {
+ id: 'var_id_2',
+ key: 'holdout_variation_2',
+ variables: []
+ }
+ ],
+ trafficAllocation: [
+ {
+ entityId: 'var_id_2',
+ endOfRange: 1000
+ }
+ ]
+ },
+ {
+ id: 'holdout_id_3',
+ key: 'holdout_3',
+ status: 'Draft',
+ includeFlags: ['feature_1'],
+ excludeFlags: [],
+ audienceIds: [],
+ audienceConditions: [],
+ variations: [
+ {
+ id: 'var_id_2',
+ key: 'holdout_variation_2',
+ variables: []
+ }
+ ],
+ trafficAllocation: [
+ {
+ entityId: 'var_id_2',
+ endOfRange: 1000
+ }
+ ]
+ }
+ ];
+
+ return datafile;
+}
+
+describe('createProjectConfig - holdouts, feature toggle is on', () => {
+ beforeAll(() => {
+ mockHoldoutToggle.mockReturnValue(true);
+ });
+
+ afterAll(() => {
+ mockHoldoutToggle.mockReset();
+ });
+
+ it('should populate holdouts fields correctly', function() {
+ const datafile = getHoldoutDatafile();
+
+ mockHoldoutToggle.mockReturnValue(true);
+
+ const configObj = projectConfig.createProjectConfig(JSON.parse(JSON.stringify(datafile)));
+
+ expect(configObj.holdouts).toHaveLength(3);
+ configObj.holdouts.forEach((holdout, i) => {
+ expect(holdout).toEqual(expect.objectContaining(datafile.holdouts[i]));
+ expect(holdout.variationKeyMap).toEqual(
+ keyBy(datafile.holdouts[i].variations, 'key')
+ );
+ });
+
+ expect(configObj.holdoutIdMap).toEqual({
+ holdout_id_1: configObj.holdouts[0],
+ holdout_id_2: configObj.holdouts[1],
+ holdout_id_3: configObj.holdouts[2],
+ });
+
+ expect(configObj.globalHoldouts).toHaveLength(2);
+ expect(configObj.globalHoldouts).toEqual([
+ configObj.holdouts[0], // holdout_1 has empty includeFlags
+ configObj.holdouts[1] // holdout_2 has empty includeFlags
+ ]);
+
+ expect(configObj.includedHoldouts).toEqual({
+ feature_1: [configObj.holdouts[2]], // holdout_3 includes feature_1
+ });
+
+ expect(configObj.excludedHoldouts).toEqual({
+ feature_3: [configObj.holdouts[1]] // holdout_2 excludes feature_3
+ });
+
+ expect(configObj.flagHoldoutsMap).toEqual({});
+ });
+
+ it('should handle empty holdouts array', function() {
+ const datafile = testDatafile.getTestProjectConfig();
+
+ const configObj = projectConfig.createProjectConfig(datafile);
+
+ expect(configObj.holdouts).toEqual([]);
+ expect(configObj.holdoutIdMap).toEqual({});
+ expect(configObj.globalHoldouts).toEqual([]);
+ expect(configObj.includedHoldouts).toEqual({});
+ expect(configObj.excludedHoldouts).toEqual({});
+ expect(configObj.flagHoldoutsMap).toEqual({});
+ });
+
+ it('should handle undefined includeFlags and excludeFlags in holdout', function() {
+ const datafile = getHoldoutDatafile();
+ datafile.holdouts[0].includeFlags = undefined;
+ datafile.holdouts[0].excludeFlags = undefined;
+
+ const configObj = projectConfig.createProjectConfig(JSON.parse(JSON.stringify(datafile)));
+
+ expect(configObj.holdouts).toHaveLength(3);
+ expect(configObj.holdouts[0].includeFlags).toEqual([]);
+ expect(configObj.holdouts[0].excludeFlags).toEqual([]);
+ });
+});
+
+describe('getHoldoutsForFlag: feature toggle is on', () => {
+ beforeAll(() => {
+ mockHoldoutToggle.mockReturnValue(true);
+ });
+
+ afterAll(() => {
+ mockHoldoutToggle.mockReset();
+ });
+
+ it('should return all applicable holdouts for a flag', () => {
+ const datafile = getHoldoutDatafile();
+ const configObj = projectConfig.createProjectConfig(JSON.parse(JSON.stringify(datafile)));
+
+ const feature1Holdouts = getHoldoutsForFlag(configObj, 'feature_1');
+ expect(feature1Holdouts).toHaveLength(3);
+ expect(feature1Holdouts).toEqual([
+ configObj.holdouts[0],
+ configObj.holdouts[1],
+ configObj.holdouts[2],
+ ]);
+
+ const feature2Holdouts = getHoldoutsForFlag(configObj, 'feature_2');
+ expect(feature2Holdouts).toHaveLength(2);
+ expect(feature2Holdouts).toEqual([
+ configObj.holdouts[0],
+ configObj.holdouts[1],
+ ]);
+
+ const feature3Holdouts = getHoldoutsForFlag(configObj, 'feature_3');
+ expect(feature3Holdouts).toHaveLength(1);
+ expect(feature3Holdouts).toEqual([
+ configObj.holdouts[0],
+ ]);
+ });
+});
+
describe('getExperimentId', () => {
let testData: Record;
let configObj: ProjectConfig;
diff --git a/lib/project_config/project_config.ts b/lib/project_config/project_config.ts
index e91c4743a..7ae95e3e9 100644
--- a/lib/project_config/project_config.ts
+++ b/lib/project_config/project_config.ts
@@ -34,6 +34,7 @@ import {
VariationVariable,
Integration,
FeatureVariableValue,
+ Holdout,
} from '../shared_types';
import { OdpConfig, OdpIntegrationConfig } from '../odp/odp_config';
import { Transformer } from '../utils/type';
@@ -51,6 +52,7 @@ import {
} from 'error_message';
import { SKIPPING_JSON_VALIDATION, VALID_DATAFILE } from 'log_message';
import { OptimizelyError } from '../error/optimizly_error';
+import * as featureToggle from '../feature_toggle';
interface TryCreatingProjectConfigConfig {
// TODO[OASIS-6649]: Don't use object type
@@ -70,7 +72,10 @@ interface VariableUsageMap {
[id: string]: VariationVariable;
}
+export type Region = 'US' | 'EU';
+
export interface ProjectConfig {
+ region: Region;
revision: string;
projectId: string;
sdkKey: string;
@@ -107,6 +112,12 @@ export interface ProjectConfig {
integrations: Integration[];
integrationKeyMap?: { [key: string]: Integration };
odpIntegrationConfig: OdpIntegrationConfig;
+ holdouts: Holdout[];
+ holdoutIdMap?: { [id: string]: Holdout };
+ globalHoldouts: Holdout[];
+ includedHoldouts: { [key: string]: Holdout[]; }
+ excludedHoldouts: { [key: string]: Holdout[]; }
+ flagHoldoutsMap: { [key: string]: Holdout[]; }
}
const EXPERIMENT_RUNNING_STATUS = 'Running';
@@ -155,6 +166,10 @@ function createMutationSafeDatafileCopy(datafile: any): ProjectConfig {
export const createProjectConfig = function(datafileObj?: JSON, datafileStr: string | null = null): ProjectConfig {
const projectConfig = createMutationSafeDatafileCopy(datafileObj);
+ if (!projectConfig.region) {
+ projectConfig.region = 'US'; // Default to US region if not specified
+ }
+
projectConfig.__datafileStr = datafileStr === null ? JSON.stringify(datafileObj) : datafileStr;
/*
@@ -328,9 +343,69 @@ export const createProjectConfig = function(datafileObj?: JSON, datafileStr: str
projectConfig.flagVariationsMap[flagKey] = variations;
});
+ parseHoldoutsConfig(projectConfig);
+
return projectConfig;
};
+const parseHoldoutsConfig = (projectConfig: ProjectConfig): void => {
+ if (!featureToggle.holdout()) {
+ return;
+ }
+
+ projectConfig.holdouts = projectConfig.holdouts || [];
+ projectConfig.holdoutIdMap = keyBy(projectConfig.holdouts, 'id');
+ projectConfig.globalHoldouts = [];
+ projectConfig.includedHoldouts = {};
+ projectConfig.excludedHoldouts = {};
+ projectConfig.flagHoldoutsMap = {};
+
+ projectConfig.holdouts.forEach((holdout) => {
+ if (!holdout.includeFlags) {
+ holdout.includeFlags = [];
+ }
+
+ if (!holdout.excludeFlags) {
+ holdout.excludeFlags = [];
+ }
+
+ holdout.variationKeyMap = keyBy(holdout.variations, 'key');
+ if (holdout.includeFlags.length === 0) {
+ projectConfig.globalHoldouts.push(holdout);
+
+ holdout.excludeFlags.forEach((flagKey) => {
+ if (!projectConfig.excludedHoldouts[flagKey]) {
+ projectConfig.excludedHoldouts[flagKey] = [];
+ }
+ projectConfig.excludedHoldouts[flagKey].push(holdout);
+ });
+ } else {
+ holdout.includeFlags.forEach((flagKey) => {
+ if (!projectConfig.includedHoldouts[flagKey]) {
+ projectConfig.includedHoldouts[flagKey] = [];
+ }
+ projectConfig.includedHoldouts[flagKey].push(holdout);
+ });
+ }
+ });
+}
+
+export const getHoldoutsForFlag = (projectConfig: ProjectConfig, flagKey: string): Holdout[] => {
+ if (projectConfig.flagHoldoutsMap[flagKey]) {
+ return projectConfig.flagHoldoutsMap[flagKey];
+ }
+
+ const flagHoldouts: Holdout[] = [
+ ...projectConfig.globalHoldouts.filter((holdout) => {
+ return !(projectConfig.excludedHoldouts[flagKey] || []).includes(holdout);
+ }),
+ ...(projectConfig.includedHoldouts[flagKey] || []),
+ ];
+
+ projectConfig.flagHoldoutsMap[flagKey] = flagHoldouts;
+ return flagHoldouts;
+}
+
/**
* Extract all audience segments used in this audience's conditions
* @param {Audience} audience Object representing the audience being parsed
diff --git a/lib/shared_types.ts b/lib/shared_types.ts
index 93d5d4524..3d3492a2c 100644
--- a/lib/shared_types.ts
+++ b/lib/shared_types.ts
@@ -64,6 +64,7 @@ export interface BucketerParams {
variationIdMap: { [id: string]: Variation };
logger?: LoggerFacade;
bucketingId: string;
+ validateEntity?: boolean;
}
export interface DecisionResponse {
@@ -150,17 +151,20 @@ export interface Variation {
variables?: VariationVariable[];
}
-export interface Experiment {
+export interface ExperimentCore {
id: string;
key: string;
variations: Variation[];
variationKeyMap: { [key: string]: Variation };
- groupId?: string;
- layerId: string;
- status: string;
audienceConditions: Array;
audienceIds: string[];
trafficAllocation: TrafficAllocation[];
+}
+
+export interface Experiment extends ExperimentCore {
+ layerId: string;
+ groupId?: string;
+ status: string;
forcedVariations?: { [key: string]: string };
isRollout?: boolean;
cmab?: {
@@ -169,6 +173,14 @@ export interface Experiment {
};
}
+export type HoldoutStatus = 'Draft' | 'Running' | 'Concluded' | 'Archived';
+
+export interface Holdout extends ExperimentCore {
+ status: HoldoutStatus;
+ includeFlags: string[];
+ excludeFlags: string[];
+}
+
export enum VariableType {
BOOLEAN = 'boolean',
DOUBLE = 'double',