@featurevisor/core
Version:
Core package of Featurevisor for Node.js usage
521 lines (430 loc) • 15.7 kB
text/typescript
import type { Condition, FeatureKey, SegmentKey, AttributeKey } from "@featurevisor/types";
import { Dependencies } from "../dependencies";
import { Plugin } from "../cli";
import {
extractAttributeKeysFromConditions,
extractSegmentKeysFromGroupSegments,
} from "../utils/extractKeys";
export interface UsageInFeatures {
[featureKey: string]: {
features: Set<FeatureKey>;
segments: Set<SegmentKey>;
attributes: Set<AttributeKey>;
};
}
export async function findAllUsageInFeatures(deps: Dependencies): Promise<UsageInFeatures> {
const { datasource, projectConfig } = deps;
const usageInFeatures: UsageInFeatures = {};
const featureKeys = await datasource.listFeatures();
for (const featureKey of featureKeys) {
const feature = await datasource.readFeature(featureKey);
usageInFeatures[featureKey] = {
features: new Set<FeatureKey>(),
segments: new Set<SegmentKey>(),
attributes: new Set<AttributeKey>(),
};
// required
if (feature.required) {
feature.required.forEach((required) => {
if (typeof required === "string") {
usageInFeatures[featureKey].features.add(required);
} else if (typeof required === "object" && required.key) {
usageInFeatures[featureKey].features.add(required.key);
}
});
}
// bucketBy
if (feature.bucketBy) {
if (typeof feature.bucketBy === "string") {
usageInFeatures[featureKey].attributes.add(feature.bucketBy);
} else if (Array.isArray(feature.bucketBy)) {
feature.bucketBy.forEach((b) => usageInFeatures[featureKey].attributes.add(b));
} else if (typeof feature.bucketBy === "object" && feature.bucketBy.or) {
feature.bucketBy.or.forEach((b) => usageInFeatures[featureKey].attributes.add(b));
}
}
// variable overrides inside variations
if (feature.variations) {
feature.variations.forEach((variation) => {
if (variation.variableOverrides) {
Object.keys(variation.variableOverrides).forEach((variableKey) => {
const overrides = variation.variableOverrides?.[variableKey];
if (overrides) {
overrides.forEach((override) => {
if (override.segments) {
extractSegmentKeysFromGroupSegments(override.segments).forEach((segmentKey) =>
usageInFeatures[featureKey].segments.add(segmentKey),
);
}
if (override.conditions) {
extractAttributeKeysFromConditions(override.conditions).forEach((attributeKey) =>
usageInFeatures[featureKey].attributes.add(attributeKey),
);
}
});
}
});
}
});
}
// with environments
if (Array.isArray(projectConfig.environments)) {
projectConfig.environments.forEach((environment) => {
// force
if (feature.force && feature.force[environment]) {
feature.force[environment].forEach((force) => {
if (force.segments) {
extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) =>
usageInFeatures[featureKey].segments.add(segmentKey),
);
}
if (force.conditions) {
extractAttributeKeysFromConditions(force.conditions).forEach((attributeKey) =>
usageInFeatures[featureKey].attributes.add(attributeKey),
);
}
});
}
// rules
if (feature.rules && feature.rules[environment]) {
feature.rules[environment].forEach((rule) => {
extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) =>
usageInFeatures[featureKey].segments.add(segmentKey),
);
});
}
});
}
// no environments
if (projectConfig.environments === false) {
// force
if (Array.isArray(feature.force)) {
feature.force.forEach((force) => {
if (force.segments) {
extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) =>
usageInFeatures[featureKey].segments.add(segmentKey),
);
}
if (force.conditions) {
extractAttributeKeysFromConditions(force.conditions).forEach((attributeKey) =>
usageInFeatures[featureKey].attributes.add(attributeKey),
);
}
});
}
// rules
if (Array.isArray(feature.rules)) {
feature.rules.forEach((rule) => {
extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) =>
usageInFeatures[featureKey].segments.add(segmentKey),
);
});
}
}
}
return usageInFeatures;
}
export interface UsageInSegments {
[segmentKey: string]: {
attributes: Set<AttributeKey>;
};
}
export async function findAllUsageInSegments(deps: Dependencies): Promise<UsageInSegments> {
const { datasource } = deps;
const usageInSegments: UsageInSegments = {};
const segmentKeys = await datasource.listSegments();
for (const segmentKey of segmentKeys) {
const segment = await datasource.readSegment(segmentKey);
usageInSegments[segmentKey] = {
attributes: new Set<AttributeKey>(),
};
extractAttributeKeysFromConditions(segment.conditions as Condition | Condition[]).forEach(
(attributeKey) => {
usageInSegments[segmentKey].attributes.add(attributeKey);
},
);
}
return usageInSegments;
}
export async function findFeatureUsage(
usageInFeatures: UsageInFeatures,
searchFeatureKey: FeatureKey,
): Promise<Set<FeatureKey>> {
const usedInFeatures = new Set<FeatureKey>();
for (const featureKey in usageInFeatures) {
if (usageInFeatures[featureKey].features.has(searchFeatureKey)) {
usedInFeatures.add(featureKey);
}
}
return usedInFeatures;
}
export async function findSegmentUsage(
usageInFeatures: UsageInFeatures,
segmentKey: SegmentKey,
): Promise<Set<FeatureKey>> {
const usedInFeatures = new Set<FeatureKey>();
for (const featureKey in usageInFeatures) {
if (usageInFeatures[featureKey].segments.has(segmentKey)) {
usedInFeatures.add(featureKey);
}
}
return usedInFeatures;
}
export interface AttributeUsage {
features: Set<FeatureKey>;
segments: Set<SegmentKey>;
}
export async function findAttributeUsage(
usageInFeatures: UsageInFeatures,
usageInSegments: UsageInSegments,
attributeKey: AttributeKey,
): Promise<AttributeUsage> {
const usedIn: AttributeUsage = {
features: new Set<FeatureKey>(),
segments: new Set<SegmentKey>(),
};
for (const featureKey in usageInFeatures) {
if (usageInFeatures[featureKey].attributes.has(attributeKey)) {
usedIn.features.add(featureKey);
}
}
for (const segmentKey in usageInSegments) {
if (usageInSegments[segmentKey].attributes.has(attributeKey)) {
usedIn.segments.add(segmentKey);
}
}
return usedIn;
}
export async function findUnusedSegments(
deps: Dependencies,
usageInFeatures: UsageInFeatures,
): Promise<Set<SegmentKey>> {
const { datasource } = deps;
const unusedSegments = new Set<SegmentKey>();
const allSegmentKeys = await datasource.listSegments();
const usedSegmentKeys = new Set<SegmentKey>();
for (const featureKey in usageInFeatures) {
usageInFeatures[featureKey].segments.forEach((segmentKey) => {
usedSegmentKeys.add(segmentKey);
});
}
allSegmentKeys.forEach((segmentKey) => {
if (!usedSegmentKeys.has(segmentKey)) {
unusedSegments.add(segmentKey);
}
});
return unusedSegments;
}
export async function findUnusedAttributes(
deps: Dependencies,
usageInFeatures: UsageInFeatures,
usageInSegments: UsageInSegments,
): Promise<Set<AttributeKey>> {
const { datasource } = deps;
const unusedAttributes = new Set<AttributeKey>();
const allAttributeKeys = await datasource.listAttributes();
const usedAttributeKeys = new Set<AttributeKey>();
for (const featureKey in usageInFeatures) {
usageInFeatures[featureKey].attributes.forEach((attributeKey) => {
usedAttributeKeys.add(attributeKey);
});
}
for (const segmentKey in usageInSegments) {
usageInSegments[segmentKey].attributes.forEach((attributeKey) => {
usedAttributeKeys.add(attributeKey);
});
}
allAttributeKeys.forEach((attributeKey) => {
if (!usedAttributeKeys.has(attributeKey)) {
unusedAttributes.add(attributeKey);
}
});
return unusedAttributes;
}
export interface FindUsageOptions {
feature?: string;
segment?: string;
attribute?: string;
unusedSegments?: boolean;
unusedAttributes?: boolean;
authors?: boolean;
}
export async function findUsageInProject(deps: Dependencies, options: FindUsageOptions) {
const { datasource } = deps;
console.log("");
const usageInFeatures = await findAllUsageInFeatures(deps);
const usageInSegments = await findAllUsageInSegments(deps);
// feature
if (options.feature) {
const usedInFeatures = await findFeatureUsage(usageInFeatures, options.feature);
if (usedInFeatures.size === 0) {
console.log(`Feature "${options.feature}" is not used in any features.`);
} else {
console.log(`Feature "${options.feature}" is used in the following features:\n`);
for (const featureKey of Array.from(usedInFeatures)) {
if (options.authors) {
const entries = await datasource.listHistoryEntries("feature", featureKey);
const authors = Array.from(new Set(entries.map((entry) => entry.author)));
console.log(` - ${featureKey} (Authors: ${authors.join(", ")})`);
} else {
console.log(` - ${featureKey}`);
}
}
}
return;
}
// segment
if (options.segment) {
const usedInFeatures = await findSegmentUsage(usageInFeatures, options.segment);
if (usedInFeatures.size === 0) {
console.log(`Segment "${options.segment}" is not used in any features.`);
} else {
console.log(`Segment "${options.segment}" is used in the following features:\n`);
for (const featureKey of Array.from(usedInFeatures)) {
if (options.authors) {
const entries = await datasource.listHistoryEntries("feature", featureKey);
const authors = Array.from(new Set(entries.map((entry) => entry.author)));
console.log(` - ${featureKey} (Authors: ${authors.join(", ")})`);
} else {
console.log(` - ${featureKey}`);
}
}
}
return;
}
// attribute
if (options.attribute) {
const usedIn = await findAttributeUsage(usageInFeatures, usageInSegments, options.attribute);
if (usedIn.features.size === 0 && usedIn.segments.size === 0) {
console.log(`Attribute "${options.attribute}" is not used in any features or segments.`);
return;
}
if (usedIn.segments.size > 0) {
console.log(`Attribute "${options.attribute}" is used in the following segments:\n`);
for (const segmentKey of Array.from(usedIn.segments)) {
if (options.authors) {
const entries = await datasource.listHistoryEntries("segment", segmentKey);
const authors = Array.from(new Set(entries.map((entry) => entry.author)));
console.log(` - ${segmentKey} (Authors: ${authors.join(", ")})`);
} else {
console.log(` - ${segmentKey}`);
}
}
// features affected by above segments
const affectedFeatures = new Set<FeatureKey>();
for (const segmentKey of Array.from(usedIn.segments)) {
const featureKeys = await findSegmentUsage(usageInFeatures, segmentKey);
featureKeys.forEach((featureKey) => affectedFeatures.add(featureKey));
}
if (affectedFeatures.size > 0) {
console.log(`\nSegments above are used in the following features:\n`);
for (const featureKey of Array.from(affectedFeatures)) {
if (options.authors) {
const entries = await datasource.listHistoryEntries("feature", featureKey);
const authors = Array.from(new Set(entries.map((entry) => entry.author)));
console.log(` - ${featureKey} (Authors: ${authors.join(", ")})`);
} else {
console.log(` - ${featureKey}`);
}
}
console.log("");
}
}
if (usedIn.features.size > 0) {
console.log(`Attribute "${options.attribute}" is used directly in the following features:\n`);
for (const featureKey of Array.from(usedIn.features)) {
if (options.authors) {
const entries = await datasource.listHistoryEntries("feature", featureKey);
const authors = Array.from(new Set(entries.map((entry) => entry.author)));
console.log(` - ${featureKey} (Authors: ${authors.join(", ")})`);
} else {
console.log(` - ${featureKey}`);
}
}
console.log("");
}
return;
}
// unused segments
if (options.unusedSegments) {
const unusedSegments = await findUnusedSegments(deps, usageInFeatures);
if (unusedSegments.size === 0) {
console.log("No unused segments found.");
} else {
console.log("Unused segments:\n");
for (const segmentKey of Array.from(unusedSegments)) {
if (options.authors) {
const entries = await datasource.listHistoryEntries("segment", segmentKey);
const authors = Array.from(new Set(entries.map((entry) => entry.author)));
console.log(` - ${segmentKey} (Authors: ${authors.join(", ")})`);
} else {
console.log(` - ${segmentKey}`);
}
}
}
return;
}
// unused attributes
if (options.unusedAttributes) {
const unusedAttributes = await findUnusedAttributes(deps, usageInFeatures, usageInSegments);
if (unusedAttributes.size === 0) {
console.log("No unused attributes found.");
} else {
console.log("Unused attributes:\n");
for (const attributeKey of Array.from(unusedAttributes)) {
if (options.authors) {
const entries = await datasource.listHistoryEntries("attribute", attributeKey);
const authors = Array.from(new Set(entries.map((entry) => entry.author)));
console.log(` - ${attributeKey} (Authors: ${authors.join(", ")})`);
} else {
console.log(` - ${attributeKey}`);
}
}
}
return;
}
console.log("Please specify a segment or attribute.");
}
export const findUsagePlugin: Plugin = {
command: "find-usage",
handler: async ({ rootDirectoryPath, projectConfig, datasource, parsed }) => {
await findUsageInProject(
{
rootDirectoryPath,
projectConfig,
datasource,
options: parsed,
},
{
feature: parsed.feature,
segment: parsed.segment,
attribute: parsed.attribute,
unusedSegments: parsed.unusedSegments,
unusedAttributes: parsed.unusedAttributes,
authors: parsed.authors,
},
);
},
examples: [
{
command: "find-usage --segment=<segmentKey>",
description: "Find usage of a segment",
},
{
command: "find-usage --attribute=<attributeKey>",
description: "Find usage of an attribute",
},
{
command: "find-usage --unused-segments",
description: "Find unused segments",
},
{
command: "find-usage --unused-attributes",
description: "Find unused attributes",
},
{
command: "find-usage --authors",
description: "List authors of the usage",
},
],
};