1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-11-01 19:07:38 +01:00
unleash.unleash/src/lib/addons/feature-event-formatter-md.ts
Nicholas Lydon 4f14549fa1
Detailed slack notifications on feature toggle update (#3155)
## About the changes
Adds more specifics to addon notifications when a flexibleRollout is
updated.

- [x] Specific text for all strategy types
- [x] Add constraint differences for all strategy types 

<!-- Does it close an issue? Multiple? -->
Closes [#3140](https://github.com/Unleash/unleash/issues/3140)
2023-02-27 09:56:56 +00:00

362 lines
12 KiB
TypeScript

import {
FEATURE_ARCHIVED,
FEATURE_CREATED,
FEATURE_ENVIRONMENT_DISABLED,
FEATURE_ENVIRONMENT_ENABLED,
FEATURE_METADATA_UPDATED,
FEATURE_PROJECT_CHANGE,
FEATURE_REVIVED,
FEATURE_STALE_OFF,
FEATURE_STALE_ON,
FEATURE_STRATEGY_ADD,
FEATURE_STRATEGY_REMOVE,
FEATURE_STRATEGY_UPDATE,
FEATURE_UPDATED,
FEATURE_VARIANTS_UPDATED,
IConstraint,
IEvent,
} from '../types';
export interface FeatureEventFormatter {
format: (event: IEvent) => string;
featureLink: (event: IEvent) => string;
}
export enum LinkStyle {
SLACK,
MD,
}
export class FeatureEventFormatterMd implements FeatureEventFormatter {
private readonly unleashUrl: string;
private readonly linkStyle: LinkStyle;
constructor(unleashUrl: string, linkStyle: LinkStyle = LinkStyle.MD) {
this.unleashUrl = unleashUrl;
this.linkStyle = linkStyle;
}
generateArchivedText(event: IEvent): string {
const { createdBy, type } = event;
const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived';
const feature = this.generateFeatureLink(event);
return ` ${createdBy} just ${action} feature toggle *${feature}*`;
}
generateFeatureLink(event: IEvent): string {
if (this.linkStyle === LinkStyle.SLACK) {
return `<${this.featureLink(event)}|${event.featureName}>`;
} else {
return `[${event.featureName}](${this.featureLink(event)})`;
}
}
generateStaleText(event: IEvent): string {
const { createdBy, type } = event;
const isStale = type === FEATURE_STALE_ON;
const feature = this.generateFeatureLink(event);
if (isStale) {
return `${createdBy} marked ${feature} as stale and this feature toggle is now *ready to be removed* from the code.`;
}
return `${createdBy} removed the stale marking on *${feature}*.`;
}
generateEnvironmentToggleText(event: IEvent): string {
const { createdBy, environment, type, project } = event;
const toggleStatus =
type === FEATURE_ENVIRONMENT_ENABLED ? 'enabled' : 'disabled';
const feature = this.generateFeatureLink(event);
return `${createdBy} *${toggleStatus}* ${feature} in *${environment}* environment in project *${project}*`;
}
generateStrategyChangeText(event: IEvent): string {
const { createdBy, environment, project, data, preData } = event;
const feature = this.generateFeatureLink(event);
const strategyText = () => {
switch (data.name) {
case 'flexibleRollout':
return this.flexibleRolloutStrategyChangeText(
preData,
data,
environment,
);
case 'default':
return this.defaultStrategyChangeText(
preData,
data,
environment,
);
case 'userWithId':
return this.userWithIdStrategyChangeText(
preData,
data,
environment,
);
case 'remoteAddress':
return this.remoteAddressStrategyChangeText(
preData,
data,
environment,
);
case 'applicationHostname':
return this.applicationHostnameStrategyChangeText(
preData,
data,
environment,
);
default:
return `by updating strategy ${data?.name} in *${environment}*`;
}
};
return `${createdBy} updated *${feature}* in project *${project}* ${strategyText()}`;
}
private applicationHostnameStrategyChangeText(
preData,
data,
environment: string | undefined,
) {
return this.listOfValuesStrategyChangeText(
preData,
data,
environment,
'hostNames',
);
}
private remoteAddressStrategyChangeText(
preData,
data,
environment: string | undefined,
) {
return this.listOfValuesStrategyChangeText(
preData,
data,
environment,
'IPs',
);
}
private userWithIdStrategyChangeText(
preData,
data,
environment: string | undefined,
) {
return this.listOfValuesStrategyChangeText(
preData,
data,
environment,
'userIds',
);
}
private listOfValuesStrategyChangeText(
preData,
data,
environment: string | undefined,
propertyName: string,
) {
const userIdText = (values) =>
values.length === 0
? `empty set of ${propertyName}`
: `[${values}]`;
const usersText =
preData.parameters[propertyName] === data.parameters[propertyName]
? ''
: ` ${propertyName} from ${userIdText(
preData.parameters[propertyName],
)} to ${userIdText(data.parameters[propertyName])}`;
const constraintText = this.constraintChangeText(
preData.constraints,
data.constraints,
);
const strategySpecificText = [usersText, constraintText]
.filter((x) => x.length)
.join(';');
return `by updating strategy ${data?.name} in *${environment}*${strategySpecificText}`;
}
private flexibleRolloutStrategyChangeText(
preData,
data,
environment: string | undefined,
) {
const {
rollout: oldRollout,
stickiness: oldStickiness,
groupId: oldGroupId,
} = preData.parameters;
const { rollout, stickiness, groupId } = data.parameters;
const stickinessText =
oldStickiness === stickiness
? ''
: ` stickiness from ${oldStickiness} to ${stickiness}`;
const rolloutText =
oldRollout === rollout
? ''
: ` rollout from ${oldRollout}% to ${rollout}%`;
const groupIdText =
oldGroupId === groupId
? ''
: ` groupId from ${oldGroupId} to ${groupId}`;
const constraintText = this.constraintChangeText(
preData.constraints,
data.constraints,
);
const strategySpecificText = [
stickinessText,
rolloutText,
groupIdText,
constraintText,
]
.filter((txt) => txt.length)
.join(';');
return `by updating strategy ${data?.name} in *${environment}*${strategySpecificText}`;
}
private defaultStrategyChangeText(
preData,
data,
environment: string | undefined,
) {
return `by updating strategy ${
data?.name
} in *${environment}*${this.constraintChangeText(
preData.constraints,
data.constraints,
)}`;
}
private constraintChangeText(
oldConstraints: IConstraint[],
newConstraints: IConstraint[],
) {
const formatConstraints = (constraints: IConstraint[]) => {
const constraintOperatorDescriptions = {
IN: 'is one of',
NOT_IN: 'is not one of',
STR_CONTAINS: 'is a string that contains',
STR_STARTS_WITH: 'is a string that starts with',
STR_ENDS_WITH: 'is a string that ends with',
NUM_EQ: 'is a number equal to',
NUM_GT: 'is a number greater than',
NUM_GTE: 'is a number greater than or equal to',
NUM_LT: 'is a number less than',
NUM_LTE: 'is a number less than or equal to',
DATE_BEFORE: 'is a date before',
DATE_AFTER: 'is a date after',
SEMVER_EQ: 'is a SemVer equal to',
SEMVER_GT: 'is a SemVer greater than',
SEMVER_LT: 'is a SemVer less than',
};
const formatConstraint = (constraint: IConstraint) => {
const val = constraint.hasOwnProperty('value')
? constraint.value
: `(${constraint.values.join(',')})`;
const operator = constraintOperatorDescriptions.hasOwnProperty(
constraint.operator,
)
? constraintOperatorDescriptions[constraint.operator]
: constraint.operator;
return `${constraint.contextName} ${
constraint.inverted ? 'not ' : ''
}${operator} ${val}`;
};
return constraints.length === 0
? 'empty set of constraints'
: `[${constraints.map(formatConstraint).join(', ')}]`;
};
const oldConstraintText = formatConstraints(oldConstraints);
const newConstraintText = formatConstraints(newConstraints);
return oldConstraintText === newConstraintText
? ''
: ` constraints from ${oldConstraintText} to ${newConstraintText}`;
}
generateStrategyRemoveText(event: IEvent): string {
const { createdBy, environment, project, preData } = event;
const feature = this.generateFeatureLink(event);
return `${createdBy} updated *${feature}* in project *${project}* by removing strategy ${preData?.name} in *${environment}*`;
}
generateStrategyAddText(event: IEvent): string {
const { createdBy, environment, project, data } = event;
const feature = this.generateFeatureLink(event);
return `${createdBy} updated *${feature}* in project *${project}* by adding strategy ${data?.name} in *${environment}*`;
}
generateMetadataText(event: IEvent): string {
const { createdBy, project } = event;
const feature = this.generateFeatureLink(event);
return `${createdBy} updated the metadata for ${feature} in project *${project}*`;
}
generateProjectChangeText(event: IEvent): string {
const { createdBy, project, featureName } = event;
return `${createdBy} moved ${featureName} to ${project}`;
}
featureLink(event: IEvent): string {
const { type, project = '', featureName } = event;
if (type === FEATURE_ARCHIVED) {
if (project) {
return `${this.unleashUrl}/projects/${project}/archive`;
}
return `${this.unleashUrl}/archive`;
}
return `${this.unleashUrl}/projects/${project}/features/${featureName}`;
}
getAction(type: string): string {
switch (type) {
case FEATURE_CREATED:
return 'created';
case FEATURE_UPDATED:
return 'updated';
case FEATURE_VARIANTS_UPDATED:
return 'updated variants for';
default:
return type;
}
}
defaultText(event: IEvent): string {
const { createdBy, project, type } = event;
const action = this.getAction(type);
const feature = this.generateFeatureLink(event);
return `${createdBy} ${action} feature toggle ${feature} in project *${project}*`;
}
format(event: IEvent): string {
switch (event.type) {
case FEATURE_ARCHIVED:
case FEATURE_REVIVED:
return this.generateArchivedText(event);
case FEATURE_STALE_ON:
case FEATURE_STALE_OFF:
return this.generateStaleText(event);
case FEATURE_ENVIRONMENT_DISABLED:
case FEATURE_ENVIRONMENT_ENABLED:
return this.generateEnvironmentToggleText(event);
case FEATURE_STRATEGY_REMOVE:
return this.generateStrategyRemoveText(event);
case FEATURE_STRATEGY_ADD:
return this.generateStrategyAddText(event);
case FEATURE_STRATEGY_UPDATE:
return this.generateStrategyChangeText(event);
case FEATURE_METADATA_UPDATED:
return this.generateMetadataText(event);
case FEATURE_PROJECT_CHANGE:
return this.generateProjectChangeText(event);
default:
return this.defaultText(event);
}
}
}