diff --git a/README.md b/README.md index 535763e28..42883ac17 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ $ npm install -g @devcycle/cli $ dvc COMMAND running command... $ dvc (--version) -@devcycle/cli/5.20.3 linux-x64 node-v22.12.0 +@devcycle/cli/5.20.3 darwin-arm64 node-v20.11.1 $ dvc --help [COMMAND] USAGE $ dvc COMMAND diff --git a/oclif.manifest.json b/oclif.manifest.json index 9958f2a35..91497da79 100644 --- a/oclif.manifest.json +++ b/oclif.manifest.json @@ -1,5 +1,5 @@ { - "version": "5.20.2", + "version": "5.20.3", "commands": { "authCommand": { "id": "authCommand", diff --git a/src/api/features.ts b/src/api/features.ts index be5efc20b..2c242b8d7 100644 --- a/src/api/features.ts +++ b/src/api/features.ts @@ -18,6 +18,7 @@ export const fetchFeatures = async ( page?: number perPage?: number search?: string + staleness?: string } = {}, ): Promise => { const response = await apiClient.get(FEATURE_URL, { diff --git a/src/api/zodClient.ts b/src/api/zodClient.ts index 30dd49d21..46a8258dd 100644 --- a/src/api/zodClient.ts +++ b/src/api/zodClient.ts @@ -27,11 +27,17 @@ const ObfuscationSettings = z.object({ enabled: z.boolean(), required: z.boolean(), }) +const StalenessSettings = z + .object({ + enabled: z.boolean(), + }) + .optional() const ProjectSettings = z.object({ edgeDB: EdgeDBSettings, optIn: OptInSettings, sdkTypeVisibility: SDKTypeVisibilitySettings, obfuscation: ObfuscationSettings, + staleness: StalenessSettings, }) const CreateProjectDto = z.object({ name: z.string().max(100), @@ -290,12 +296,12 @@ const UpdateAudienceDto = z }) .partial() const VariableValidationEntity = z.object({ - schemaType: z.object({}).partial(), - enumValues: z.object({}).partial().optional(), + schemaType: z.string(), + enumValues: z.array(z.string()).optional(), regexPattern: z.string().optional(), jsonSchema: z.string().optional(), description: z.string(), - exampleValue: z.object({}).partial(), + exampleValue: z.any(), }) const CreateVariableDto = z.object({ name: z.string().max(100).optional(), @@ -540,7 +546,7 @@ const Target = z.object({ _id: z.string(), name: z.string().optional(), audience: TargetAudience, - rollout: Rollout.optional(), + rollout: Rollout.nullable().optional(), distribution: z.array(TargetDistribution), }) const FeatureConfig = z.object({ @@ -556,7 +562,7 @@ const FeatureConfig = z.object({ const UpdateTargetDto = z.object({ _id: z.string().optional(), name: z.string().optional(), - rollout: Rollout.optional(), + rollout: Rollout.nullable().optional(), distribution: z.array(TargetDistribution), audience: TargetAudience, }) @@ -615,6 +621,16 @@ const Feature = z.object({ readonly: z.boolean(), settings: FeatureSettings.partial().optional(), sdkVisibility: FeatureSDKVisibility.optional(), + staleness: z + .object({ + stale: z.boolean(), + updatedAt: z.string().datetime().optional(), + disabled: z.boolean().optional(), + snoozedUntil: z.string().datetime().optional(), + reason: z.string().optional(), + metaData: z.record(z.string(), z.unknown()).optional(), + }) + .optional(), }) const FeatureDataPoint = z.object({ values: z.object({}).partial(), diff --git a/src/commands/generate/types.ts b/src/commands/generate/types.ts index 38edf6975..18ccd80be 100644 --- a/src/commands/generate/types.ts +++ b/src/commands/generate/types.ts @@ -7,7 +7,10 @@ import { OrganizationMember, fetchOrganizationMembers } from '../../api/members' import { upperCase } from 'lodash' import { createHash } from 'crypto' import path from 'path' -import { fetchAllCompletedOrArchivedFeatures } from '../../api/features' +import { + fetchAllCompletedOrArchivedFeatures, + fetchFeatures, +} from '../../api/features' import { fetchCustomProperties } from '../../api/customProperties' const reactImports = (oldRepos: boolean, strictCustomData: boolean) => { @@ -176,10 +179,23 @@ export default class GenerateTypes extends Base { ) } - this.features = await fetchAllCompletedOrArchivedFeatures( - this.authToken, - this.projectKey, - ) + if (this.project.settings?.staleness?.enabled) { + const [completedFeatures, staleFeatures] = await Promise.all([ + fetchAllCompletedOrArchivedFeatures( + this.authToken, + this.projectKey, + ), + fetchFeatures(this.authToken, this.projectKey, { + staleness: 'all', + }), + ]) + this.features = [...completedFeatures, ...staleFeatures] + } else { + this.features = await fetchAllCompletedOrArchivedFeatures( + this.authToken, + this.projectKey, + ) + } const variables = await fetchAllVariables( this.authToken, @@ -294,12 +310,25 @@ export default class GenerateTypes extends Base { const createdDate = variable.createdAt.split('T')[0] const deprecationInfo = isVariableDeprecated(variable, this.features) + const isDeprecated = this.includeDeprecationWarnings && deprecationInfo.deprecated const deprecationWarning = isDeprecated ? `@deprecated This variable is part of ${deprecationInfo.feature?.status} feature "${deprecationInfo.feature?.name}" and should be cleaned up.\n` : '' + let staleWarning = '' + if (this.project.settings?.staleness?.enabled) { + const staleInfo = isVariableStale(variable, this.features) + const recommendedValue = getRecommendedValueForStale( + variable, + staleInfo.feature as Feature, + ) + staleWarning = staleInfo.stale + ? `@stale This variable is part of "${staleInfo.feature?.name}" feature with stale reason: ${staleInfo.feature?.staleness?.reason}. ${recommendedValue ? `Recommended value to set it to: ${recommendedValue}` : ''}\n` + : '' + } + return blockComment( descriptionText, creator, @@ -307,6 +336,7 @@ export default class GenerateTypes extends Base { indent, !this.obfuscate ? variable.key : undefined, deprecationWarning, + staleWarning, ) } @@ -385,6 +415,7 @@ export const blockComment = ( indent: boolean, key?: string, deprecationWarning?: string, + staleWarning?: string, ) => { const indentString = indent ? ' ' : '' return ( @@ -399,6 +430,7 @@ export const blockComment = ( (deprecationWarning ? `${indentString} * ${deprecationWarning}\n` : '') + + (staleWarning ? `${indentString} * ${staleWarning}\n` : '') + indentString + '*/' ) @@ -430,6 +462,39 @@ function isVariableDeprecated(variable: Variable, features: Feature[]) { return { deprecated: feature && feature.status !== 'active', feature } } +function isVariableStale(variable: Variable, features: Feature[]) { + if (!variable._feature || variable.persistent) return { stale: false } + const feature = features.find((f) => f._id === variable._feature) + return { stale: feature && feature.staleness, feature } +} + +function getRecommendedValueForStale(variable: Variable, feature: Feature) { + if (!feature) { + return variable.defaultValue + } + const reason = feature.staleness?.reason + if (reason === 'unused') { + return variable.defaultValue + } else if (reason === 'released') { + if (feature.staleness?.metaData?.releaseVariation) { + const stalenessMetaData = feature.staleness?.metaData + ?.releaseVariation as { + _variation: string + variationKey: string + variationName: string + } + const releaseVariation = feature.variations?.find( + (v) => v._id === stalenessMetaData._variation, + ) + return ( + releaseVariation?.variables?.[variable.key] || + variable.defaultValue + ) + } + } + return variable.defaultValue +} + const generateCustomDataType = ( customProperties: CustomProperty[], strict: boolean,