From 5733f5682228698c9fe8ee1b5fe27604fd4f5e94 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Fri, 1 Nov 2024 15:36:22 -0700 Subject: [PATCH 1/4] Create codeql.yml (#65) --- .github/workflows/codeql.yml | 92 ++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..587f74b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,92 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main", "Preview", "Release" ] + pull_request: + branches: [ "main", "Preview", "Release" ] + schedule: + - cron: '45 7 * * 0' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From e7119e24dfed3caae1be6d72df23068deedee299 Mon Sep 17 00:00:00 2001 From: Yan Zhang <2351748+Eskibear@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:22:22 +0800 Subject: [PATCH 2/4] validate property types of feature flags (#17) * resovle conflicts * update validation * update * update * update * update * update * update --------- Co-authored-by: Lingling Ye (from Dev Box) --- package-lock.json | 2 +- src/featureManager.ts | 17 ++-- src/featureProvider.ts | 19 +++- src/{ => schema}/model.ts | 12 --- src/schema/validator.ts | 186 ++++++++++++++++++++++++++++++++++++ test/featureManager.test.ts | 37 +++++++ test/noFilters.test.ts | 2 +- 7 files changed, 246 insertions(+), 29 deletions(-) rename src/{ => schema}/model.ts (91%) create mode 100644 src/schema/validator.ts diff --git a/package-lock.json b/package-lock.json index 0bc8262..73b3276 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "eslint": "^8.56.0", "mocha": "^10.2.0", "rimraf": "^5.0.5", - "rollup": "^4.9.4", + "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.0", "tslib": "^2.6.2", "typescript": "^5.3.3" diff --git a/src/featureManager.ts b/src/featureManager.ts index 402510f..8cb4008 100644 --- a/src/featureManager.ts +++ b/src/featureManager.ts @@ -3,7 +3,7 @@ import { TimeWindowFilter } from "./filter/TimeWindowFilter.js"; import { IFeatureFilter } from "./filter/FeatureFilter.js"; -import { RequirementType } from "./model.js"; +import { RequirementType } from "./schema/model.js"; import { IFeatureFlagProvider } from "./featureProvider.js"; import { TargetingFilter } from "./filter/TargetingFilter.js"; @@ -30,15 +30,12 @@ export class FeatureManager { // If multiple feature flags are found, the first one takes precedence. async isEnabled(featureName: string, context?: unknown): Promise { - const featureFlag = await this.#provider.getFeatureFlag(featureName); + const featureFlag = await this.#getFeatureFlag(featureName); if (featureFlag === undefined) { // If the feature is not found, then it is disabled. return false; } - // Ensure that the feature flag is in the correct format. Feature providers should validate the feature flags, but we do it here as a safeguard. - validateFeatureFlagFormat(featureFlag); - if (featureFlag.enabled !== true) { // If the feature is not explicitly enabled, then it is disabled by default. return false; @@ -75,14 +72,14 @@ export class FeatureManager { return !shortCircuitEvaluationResult; } + async #getFeatureFlag(featureName: string): Promise { + const featureFlag = await this.#provider.getFeatureFlag(featureName); + return featureFlag; + } + } interface FeatureManagerOptions { customFilters?: IFeatureFilter[]; } -function validateFeatureFlagFormat(featureFlag: any): void { - if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") { - throw new Error(`Feature flag ${featureFlag.id} has an invalid 'enabled' value.`); - } -} diff --git a/src/featureProvider.ts b/src/featureProvider.ts index c5a1aac..29f0802 100644 --- a/src/featureProvider.ts +++ b/src/featureProvider.ts @@ -2,7 +2,8 @@ // Licensed under the MIT license. import { IGettable } from "./gettable.js"; -import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model.js"; +import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./schema/model.js"; +import { validateFeatureFlag } from "./schema/validator.js"; export interface IFeatureFlagProvider { /** @@ -28,12 +29,16 @@ export class ConfigurationMapFeatureFlagProvider implements IFeatureFlagProvider } async getFeatureFlag(featureName: string): Promise { const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY); - return featureConfig?.[FEATURE_FLAGS_KEY]?.findLast((feature) => feature.id === featureName); + const featureFlag = featureConfig?.[FEATURE_FLAGS_KEY]?.findLast((feature) => feature.id === featureName); + validateFeatureFlag(featureFlag); + return featureFlag; } async getFeatureFlags(): Promise { const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY); - return featureConfig?.[FEATURE_FLAGS_KEY] ?? []; + const featureFlag = featureConfig?.[FEATURE_FLAGS_KEY] ?? []; + validateFeatureFlag(featureFlag); + return featureFlag; } } @@ -49,10 +54,14 @@ export class ConfigurationObjectFeatureFlagProvider implements IFeatureFlagProvi async getFeatureFlag(featureName: string): Promise { const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY]; - return featureFlags?.findLast((feature: FeatureFlag) => feature.id === featureName); + const featureFlag = featureFlags?.findLast((feature: FeatureFlag) => feature.id === featureName); + validateFeatureFlag(featureFlag); + return featureFlag; } async getFeatureFlags(): Promise { - return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? []; + const featureFlag = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? []; + validateFeatureFlag(featureFlag); + return featureFlag; } } diff --git a/src/model.ts b/src/schema/model.ts similarity index 91% rename from src/model.ts rename to src/schema/model.ts index 7ee9cc4..c4ff360 100644 --- a/src/model.ts +++ b/src/schema/model.ts @@ -12,14 +12,6 @@ export interface FeatureFlag { * An ID used to uniquely identify and reference the feature. */ id: string; - /** - * A description of the feature. - */ - description?: string; - /** - * A display name for the feature to use for display rather than the ID. - */ - display_name?: string; /** * A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied. */ @@ -78,10 +70,6 @@ interface Variant { * The configuration value for this feature variant. */ configuration_value?: unknown; - /** - * The path to a configuration section used as the configuration value for this feature variant. - */ - configuration_reference?: string; /** * Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None. */ diff --git a/src/schema/validator.ts b/src/schema/validator.ts new file mode 100644 index 0000000..1d63d72 --- /dev/null +++ b/src/schema/validator.ts @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Validates a feature flag object, checking if it conforms to the schema. + * @param featureFlag The feature flag object to validate. + */ +export function validateFeatureFlag(featureFlag: any): void { + if (featureFlag === undefined) { + return; // no-op if feature flag is undefined, indicating that the feature flag is not found + } + if (featureFlag === null || typeof featureFlag !== "object") { // Note: typeof null = "object" + throw new TypeError("Feature flag must be an object."); + } + if (typeof featureFlag.id !== "string") { + throw new TypeError("Feature flag 'id' must be a string."); + } + if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") { + throw new TypeError("Feature flag 'enabled' must be a boolean."); + } + if (featureFlag.conditions !== undefined) { + validateFeatureEnablementConditions(featureFlag.conditions); + } + if (featureFlag.variants !== undefined) { + validateVariants(featureFlag.variants); + } + if (featureFlag.allocation !== undefined) { + validateVariantAllocation(featureFlag.allocation); + } + if (featureFlag.telemetry !== undefined) { + validateTelemetryOptions(featureFlag.telemetry); + } +} + +function validateFeatureEnablementConditions(conditions: any) { + if (typeof conditions !== "object") { + throw new TypeError("Feature flag 'conditions' must be an object."); + } + if (conditions.requirement_type !== undefined && conditions.requirement_type !== "Any" && conditions.requirement_type !== "All") { + throw new TypeError("'requirement_type' must be 'Any' or 'All'."); + } + if (conditions.client_filters !== undefined) { + validateClientFilters(conditions.client_filters); + } +} + +function validateClientFilters(client_filters: any) { + if (!Array.isArray(client_filters)) { + throw new TypeError("Feature flag conditions 'client_filters' must be an array."); + } + + for (const filter of client_filters) { + if (typeof filter.name !== "string") { + throw new TypeError("Client filter 'name' must be a string."); + } + if (filter.parameters !== undefined && typeof filter.parameters !== "object") { + throw new TypeError("Client filter 'parameters' must be an object."); + } + } +} + +function validateVariants(variants: any) { + if (!Array.isArray(variants)) { + throw new TypeError("Feature flag 'variants' must be an array."); + } + + for (const variant of variants) { + if (typeof variant.name !== "string") { + throw new TypeError("Variant 'name' must be a string."); + } + // skip configuration_value validation as it accepts any type + if (variant.status_override !== undefined && typeof variant.status_override !== "string") { + throw new TypeError("Variant 'status_override' must be a string."); + } + if (variant.status_override !== undefined && variant.status_override !== "None" && variant.status_override !== "Enabled" && variant.status_override !== "Disabled") { + throw new TypeError("Variant 'status_override' must be 'None', 'Enabled', or 'Disabled'."); + } + } +} + +function validateVariantAllocation(allocation: any) { + if (typeof allocation !== "object") { + throw new TypeError("Variant 'allocation' must be an object."); + } + + if (allocation.default_when_disabled !== undefined && typeof allocation.default_when_disabled !== "string") { + throw new TypeError("Variant allocation 'default_when_disabled' must be a string."); + } + if (allocation.default_when_enabled !== undefined && typeof allocation.default_when_enabled !== "string") { + throw new TypeError("Variant allocation 'default_when_enabled' must be a string."); + } + if (allocation.user !== undefined) { + validateUserVariantAllocation(allocation.user); + } + if (allocation.group !== undefined) { + validateGroupVariantAllocation(allocation.group); + } + if (allocation.percentile !== undefined) { + validatePercentileVariantAllocation(allocation.percentile); + } + if (allocation.seed !== undefined && typeof allocation.seed !== "string") { + throw new TypeError("Variant allocation 'seed' must be a string."); + } +} + +function validateUserVariantAllocation(UserAllocations: any) { + if (!Array.isArray(UserAllocations)) { + throw new TypeError("Variant 'user' allocation must be an array."); + } + + for (const allocation of UserAllocations) { + if (typeof allocation !== "object") { + throw new TypeError("Elements in variant 'user' allocation must be an object."); + } + if (typeof allocation.variant !== "string") { + throw new TypeError("User allocation 'variant' must be a string."); + } + if (!Array.isArray(allocation.users)) { + throw new TypeError("User allocation 'users' must be an array."); + } + for (const user of allocation.users) { + if (typeof user !== "string") { + throw new TypeError("Elements in user allocation 'users' must be strings."); + } + } + } +} + +function validateGroupVariantAllocation(groupAllocations: any) { + if (!Array.isArray(groupAllocations)) { + throw new TypeError("Variant 'group' allocation must be an array."); + } + + for (const allocation of groupAllocations) { + if (typeof allocation !== "object") { + throw new TypeError("Elements in variant 'group' allocation must be an object."); + } + if (typeof allocation.variant !== "string") { + throw new TypeError("Group allocation 'variant' must be a string."); + } + if (!Array.isArray(allocation.groups)) { + throw new TypeError("Group allocation 'groups' must be an array."); + } + for (const group of allocation.groups) { + if (typeof group !== "string") { + throw new TypeError("Elements in group allocation 'groups' must be strings."); + } + } + } +} + +function validatePercentileVariantAllocation(percentileAllocations: any) { + if (!Array.isArray(percentileAllocations)) { + throw new TypeError("Variant 'percentile' allocation must be an array."); + } + + for (const allocation of percentileAllocations) { + if (typeof allocation !== "object") { + throw new TypeError("Elements in variant 'percentile' allocation must be an object."); + } + if (typeof allocation.variant !== "string") { + throw new TypeError("Percentile allocation 'variant' must be a string."); + } + if (typeof allocation.from !== "number" || allocation.from < 0 || allocation.from > 100) { + throw new TypeError("Percentile allocation 'from' must be a number between 0 and 100."); + } + if (typeof allocation.to !== "number" || allocation.to < 0 || allocation.to > 100) { + throw new TypeError("Percentile allocation 'to' must be a number between 0 and 100."); + } + } +} +// #endregion + +// #region Telemetry +function validateTelemetryOptions(telemetry: any) { + if (typeof telemetry !== "object") { + throw new TypeError("Feature flag 'telemetry' must be an object."); + } + if (telemetry.enabled !== undefined && typeof telemetry.enabled !== "boolean") { + throw new TypeError("Telemetry 'enabled' must be a boolean."); + } + if (telemetry.metadata !== undefined && typeof telemetry.metadata !== "object") { + throw new TypeError("Telemetry 'metadata' must be an object."); + } +} +// #endregion diff --git a/test/featureManager.test.ts b/test/featureManager.test.ts index ea9c787..0bfa331 100644 --- a/test/featureManager.test.ts +++ b/test/featureManager.test.ts @@ -72,6 +72,43 @@ describe("feature manager", () => { ]); }); + it("should evaluate features with conditions", () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { + "id": "Gamma", + "description": "", + "enabled": true, + "conditions": { + "requirement_type": "invalid type", + "client_filters": [ + { "name": "Microsoft.Targeting", "parameters": { "Audience": { "DefaultRolloutPercentage": 50 } } } + ] + } + }, + { + "id": "Delta", + "description": "", + "enabled": true, + "conditions": { + "requirement_type": "Any", + "client_filters": [ + { "name": "Microsoft.Targeting", "parameters": "invalid parameter" } + ] + } + } + ], + }); + + const provider = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager = new FeatureManager(provider); + return Promise.all([ + expect(featureManager.isEnabled("Gamma")).eventually.rejectedWith("'requirement_type' must be 'Any' or 'All'."), + expect(featureManager.isEnabled("Delta")).eventually.rejectedWith("Client filter 'parameters' must be an object.") + ]); + }); + it("should let the last feature flag win", () => { const jsonObject = { "feature_management": { diff --git a/test/noFilters.test.ts b/test/noFilters.test.ts index 55639f7..aaac209 100644 --- a/test/noFilters.test.ts +++ b/test/noFilters.test.ts @@ -61,7 +61,7 @@ describe("feature flags with no filters", () => { return Promise.all([ expect(featureManager.isEnabled("BooleanTrue")).eventually.eq(true), expect(featureManager.isEnabled("BooleanFalse")).eventually.eq(false), - expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag InvalidEnabled has an invalid 'enabled' value."), + expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag 'enabled' must be a boolean."), expect(featureManager.isEnabled("Minimal")).eventually.eq(true), expect(featureManager.isEnabled("NoEnabled")).eventually.eq(false), expect(featureManager.isEnabled("EmptyConditions")).eventually.eq(true) From 8fe0efd0d923f1f24364f53a5b20702c1f923f0f Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:30:36 +0800 Subject: [PATCH 3/4] small fix (#68) --- src/featureManager.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/featureManager.ts b/src/featureManager.ts index 8cb4008..6c02faa 100644 --- a/src/featureManager.ts +++ b/src/featureManager.ts @@ -30,7 +30,7 @@ export class FeatureManager { // If multiple feature flags are found, the first one takes precedence. async isEnabled(featureName: string, context?: unknown): Promise { - const featureFlag = await this.#getFeatureFlag(featureName); + const featureFlag = await this.#provider.getFeatureFlag(featureName); if (featureFlag === undefined) { // If the feature is not found, then it is disabled. return false; @@ -71,12 +71,6 @@ export class FeatureManager { // If we get here, then we have not found a client filter that matches the requirement type. return !shortCircuitEvaluationResult; } - - async #getFeatureFlag(featureName: string): Promise { - const featureFlag = await this.#provider.getFeatureFlag(featureName); - return featureFlag; - } - } interface FeatureManagerOptions { From 2c53e2e283fc6dc8e013aa72655e6e0392cfb613 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 6 Nov 2024 18:56:30 +0800 Subject: [PATCH 4/4] use string seed in sample feature flag --- sdk/feature-management/src/schema/model.ts | 28 +++++++++---------- .../test/sampleFeatureFlags.ts | 6 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/sdk/feature-management/src/schema/model.ts b/sdk/feature-management/src/schema/model.ts index 2e18cb6..21749a0 100644 --- a/sdk/feature-management/src/schema/model.ts +++ b/sdk/feature-management/src/schema/model.ts @@ -33,7 +33,7 @@ export interface FeatureFlag { */ telemetry?: TelemetryOptions } - + /** * The declaration of conditions used to dynamically enable the feature */ @@ -47,9 +47,9 @@ export interface FeatureFlag { */ client_filters?: ClientFilter[]; } - + export type RequirementType = "Any" | "All"; - + interface ClientFilter { /** * The name used to refer to a client filter. @@ -60,7 +60,7 @@ export interface FeatureFlag { */ parameters?: Record; } - + export interface VariantDefinition { /** * The name used to refer to a feature variant. @@ -75,7 +75,7 @@ export interface FeatureFlag { */ status_override?: "None" | "Enabled" | "Disabled"; } - + /** * Determines how variants should be allocated for the feature to various users. */ @@ -105,7 +105,7 @@ export interface FeatureFlag { */ seed?: string; } - + interface UserAllocation { /** * The name of the variant to use if the user allocation matches the current user. @@ -116,7 +116,7 @@ export interface FeatureFlag { */ users: string[]; } - + interface GroupAllocation { /** * The name of the variant to use if the group allocation matches a group the current user is in. @@ -127,7 +127,7 @@ export interface FeatureFlag { */ groups: string[]; } - + interface PercentileAllocation { /** * The name of the variant to use if the calculated percentile for the current user falls in the provided range. @@ -142,7 +142,7 @@ export interface FeatureFlag { */ to: number; } - + /** * The declaration of options used to configure telemetry for this feature. */ @@ -156,20 +156,20 @@ export interface FeatureFlag { */ metadata?: Record; } - + // Feature Management Section fed into feature manager. // Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json - + export const FEATURE_MANAGEMENT_KEY = "feature_management"; export const FEATURE_FLAGS_KEY = "feature_flags"; - + export interface FeatureManagementConfiguration { feature_management: FeatureManagement } - + /** * Declares feature management configuration. */ export interface FeatureManagement { feature_flags: FeatureFlag[]; - } \ No newline at end of file + } diff --git a/sdk/feature-management/test/sampleFeatureFlags.ts b/sdk/feature-management/test/sampleFeatureFlags.ts index 9f95b36..85f69b0 100644 --- a/sdk/feature-management/test/sampleFeatureFlags.ts +++ b/sdk/feature-management/test/sampleFeatureFlags.ts @@ -219,7 +219,7 @@ export const featureFlagsConfigurationObject = { "to": 50 } ], - "seed": 1234 + "seed": "1234" }, "telemetry": { "enabled": true @@ -241,7 +241,7 @@ export const featureFlagsConfigurationObject = { "to": 50 } ], - "seed": 12345 + "seed": "12345" }, "telemetry": { "enabled": true @@ -263,7 +263,7 @@ export const featureFlagsConfigurationObject = { "to": 100 } ], - "seed": 12345 + "seed": "12345" }, "telemetry": { "enabled": true