1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

fix: health-technical debt trends in emails (#10308)

This commit is contained in:
Tymoteusz Czech 2025-07-04 16:46:59 +02:00 committed by GitHub
parent cb4beb71ac
commit 212c538a75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 111 additions and 101 deletions

View File

@ -33,7 +33,7 @@ describe('productivityReportViewModel', () => {
}); });
describe('healthColor', () => { describe('healthColor', () => {
it('returns RED for health between 0 and 24', () => { it('returns RED for technical debt between 75 and 100', () => {
const metrics: ProductivityReportMetrics = { const metrics: ProductivityReportMetrics = {
...mockMetrics, ...mockMetrics,
health: 20, health: 20,
@ -44,10 +44,10 @@ describe('productivityReportViewModel', () => {
metrics, metrics,
}); });
expect(viewModel.healthColor()).toBe('#d93644'); expect(viewModel.technicalDebtColor()).toBe('#d93644');
}); });
it('returns ORANGE for health between 25 and 74', () => { it('returns ORANGE for technical debt between 25 and 74', () => {
const metrics: ProductivityReportMetrics = { const metrics: ProductivityReportMetrics = {
...mockMetrics, ...mockMetrics,
health: 50, health: 50,
@ -58,10 +58,10 @@ describe('productivityReportViewModel', () => {
metrics, metrics,
}); });
expect(viewModel.healthColor()).toBe('#d76500'); expect(viewModel.technicalDebtColor()).toBe('#d76500');
}); });
it('returns GREEN for health 75 or above', () => { it('returns GREEN for technical debt below 25', () => {
const metrics: ProductivityReportMetrics = { const metrics: ProductivityReportMetrics = {
...mockMetrics, ...mockMetrics,
health: 80, health: 80,
@ -72,25 +72,24 @@ describe('productivityReportViewModel', () => {
metrics, metrics,
}); });
expect(viewModel.healthColor()).toBe('#68a611'); expect(viewModel.technicalDebtColor()).toBe('#68a611');
}); });
}); });
describe('healthTrendMessage', () => { describe('technicalDebtTrendMessage', () => {
it('returns correct trend message when technica debt decreased', () => { it('returns correct trend message when technica debt decreased', () => {
const metrics: ProductivityReportMetrics = { const metrics: ProductivityReportMetrics = {
...mockMetrics, ...mockMetrics,
health: 80, health: 80,
previousMonth: { ...mockMetrics.previousMonth, health: 70 }, previousMonth: { ...mockMetrics.previousMonth, health: 70 },
}; };
const viewModel = productivityReportViewModel({ const viewModel = productivityReportViewModel({
...mockData, ...mockData,
metrics, metrics,
}); });
expect(viewModel.healthTrendMessage()).toBe( expect(viewModel.technicalDebtTrendMessage()).toMatchInlineSnapshot(
"<span style='color: #68a611'>&#9650;</span> 10% more than previous month", `"<span style='color: #68a611'>&#9660;</span> 10% less than previous month"`,
); );
}); });
@ -100,14 +99,13 @@ describe('productivityReportViewModel', () => {
health: 60, health: 60,
previousMonth: { ...mockMetrics.previousMonth, health: 70 }, previousMonth: { ...mockMetrics.previousMonth, health: 70 },
}; };
const viewModel = productivityReportViewModel({ const viewModel = productivityReportViewModel({
...mockData, ...mockData,
metrics, metrics,
}); });
expect(viewModel.healthTrendMessage()).toBe( expect(viewModel.technicalDebtTrendMessage()).toMatchInlineSnapshot(
"<span style='color: #d93644'>&#9660;</span> 10% less than previous month", `"<span style='color: #d93644'>&#9650;</span> 10% more than previous month"`,
); );
}); });
@ -117,13 +115,14 @@ describe('productivityReportViewModel', () => {
health: 70, health: 70,
previousMonth: { ...mockMetrics.previousMonth, health: 70 }, previousMonth: { ...mockMetrics.previousMonth, health: 70 },
}; };
const viewModel = productivityReportViewModel({ const viewModel = productivityReportViewModel({
...mockData, ...mockData,
metrics, metrics,
}); });
expect(viewModel.healthTrendMessage()).toBe('Same as last month'); expect(viewModel.technicalDebtTrendMessage()).toMatchInlineSnapshot(
`"Same as last month"`,
);
}); });
}); });
@ -137,18 +136,15 @@ describe('productivityReportViewModel', () => {
flagsCreated: 8, flagsCreated: 8,
}, },
}; };
const viewModel = productivityReportViewModel({ const viewModel = productivityReportViewModel({
...mockData, ...mockData,
metrics, metrics,
}); });
expect(viewModel.flagsCreatedTrendMessage()).toMatchInlineSnapshot(
expect(viewModel.flagsCreatedTrendMessage()).toBe( `"<span style='color: #68a611'>&#9650;</span> 2 more than previous month"`,
"<span style='color: #68a611'>&#9650;</span> 2 more than previous month",
); );
}); });
}); });
describe('productionUpdatedTrendMessage', () => { describe('productionUpdatedTrendMessage', () => {
it('returns correct trend message for productionUpdates decrease', () => { it('returns correct trend message for productionUpdates decrease', () => {
const metrics: ProductivityReportMetrics = { const metrics: ProductivityReportMetrics = {
@ -159,18 +155,17 @@ describe('productivityReportViewModel', () => {
productionUpdates: 8, productionUpdates: 8,
}, },
}; };
const viewModel = productivityReportViewModel({ const viewModel = productivityReportViewModel({
...mockData, ...mockData,
metrics, metrics,
}); });
expect(
expect(viewModel.productionUpdatedTrendMessage()).toBe( viewModel.productionUpdatedTrendMessage(),
"<span style='color: #d93644'>&#9660;</span> 3 less than previous month", ).toMatchInlineSnapshot(
`"<span style='color: #d93644'>&#9660;</span> 3 less than previous month"`,
); );
}); });
}); });
describe('Missing previous month data', () => { describe('Missing previous month data', () => {
it('returns no trends messages', () => { it('returns no trends messages', () => {
const metrics: ProductivityReportMetrics = { const metrics: ProductivityReportMetrics = {
@ -179,13 +174,12 @@ describe('productivityReportViewModel', () => {
productionUpdates: 5, productionUpdates: 5,
previousMonth: null, previousMonth: null,
}; };
const viewModel = productivityReportViewModel({ const viewModel = productivityReportViewModel({
...mockData, ...mockData,
metrics, metrics,
}); });
expect(viewModel.healthTrendMessage()).toBe(null); expect(viewModel.technicalDebtTrendMessage()).toBe(null);
expect(viewModel.flagsCreatedTrendMessage()).toBe(null); expect(viewModel.flagsCreatedTrendMessage()).toBe(null);
expect(viewModel.productionUpdatedTrendMessage()).toBe(null); expect(viewModel.productionUpdatedTrendMessage()).toBe(null);
}); });
@ -225,8 +219,8 @@ describe('productivityReportViewModel', () => {
metrics, metrics,
}); });
expect(viewModel.actionText()).toBe( expect(viewModel.actionText()).toMatchInlineSnapshot(
'Remember to archive stale flags to reduce technical debt and keep your project healthy', `"Remember to archive stale flags to reduce technical debt and keep your project healthy"`,
); );
}); });
@ -245,8 +239,8 @@ describe('productivityReportViewModel', () => {
metrics, metrics,
}); });
expect(viewModel.actionText()).toBe( expect(viewModel.actionText()).toMatchInlineSnapshot(
'Remember to archive stale flags to reduce technical debt and keep your project healthy', `"Remember to archive stale flags to reduce technical debt and keep your project healthy"`,
); );
}); });

View File

@ -13,6 +13,9 @@ const RED = '#d93644';
const GREEN = '#68a611'; const GREEN = '#68a611';
const ORANGE = '#d76500'; const ORANGE = '#d76500';
const ARROW_UP = '&#9650;';
const ARROW_DOWN = '&#9660;';
export const productivityReportViewModel = ({ export const productivityReportViewModel = ({
unleashUrl, unleashUrl,
userEmail, userEmail,
@ -23,69 +26,82 @@ export const productivityReportViewModel = ({
userEmail: string; userEmail: string;
userName: string; userName: string;
metrics: ProductivityReportMetrics; metrics: ProductivityReportMetrics;
}) => ({ }) => {
userName, const { health, previousMonth, flagsCreated, productionUpdates } = metrics;
userEmail, const technicalDebt = Math.max(0, 100 - health) || 0;
...metrics,
technicalDebt: Math.max(0, 100 - metrics.health).toString(), return {
unleashUrl, userName,
healthColor() { userEmail,
const healthRating = this.health; flagsCreated,
const healthColor = productionUpdates,
healthRating >= 0 && healthRating <= 24 previousMonth,
? RED health,
: healthRating >= 25 && healthRating <= 74 technicalDebt: technicalDebt.toString(),
? ORANGE unleashUrl,
: GREEN; technicalDebtColor() {
return healthColor; if (technicalDebt < 25) {
}, return GREEN;
actionText(): string | null { }
const improveMessage = if (technicalDebt < 75) {
'Remember to archive stale flags to reduce technical debt and keep your project healthy'; return ORANGE;
const previousHealth = this.previousMonth?.health || 0; }
if (this.health <= 74) { return RED;
return improveMessage; },
} actionText(): string | null {
if (this.health < previousHealth) { const improveMessage =
return improveMessage; 'Remember to archive stale flags to reduce technical debt and keep your project healthy';
} const previousHealth = previousMonth?.health || 0;
return null; if (health <= 74) {
}, return improveMessage;
healthTrendMessage() { }
return this.previousMonthText( if (health < previousHealth) {
'%', return improveMessage;
this.health, }
this.previousMonth?.health,
);
},
flagsCreatedTrendMessage() {
return this.previousMonthText(
'',
this.flagsCreated,
this.previousMonth?.flagsCreated,
);
},
productionUpdatedTrendMessage() {
return this.previousMonthText(
'',
this.productionUpdates,
this.previousMonth?.productionUpdates,
);
},
previousMonthText(
unit: '' | '%',
currentValue: number,
previousValue?: number,
) {
if (previousValue == null) {
return null; return null;
} },
if (currentValue > previousValue) { technicalDebtTrendMessage() {
return `<span style='color: ${GREEN}'>&#9650;</span> ${currentValue - previousValue}${unit} more than previous month`; if (!previousMonth || Number.isNaN(previousMonth.health)) {
} return null;
if (previousValue > currentValue) { }
return `<span style='color: ${RED}'>&#9660;</span> ${previousValue - currentValue}${unit} less than previous month`; const previousTechnicalDebt = Math.max(
} 0,
return `Same as last month`; 100 - previousMonth.health,
}, );
});
if (technicalDebt > previousTechnicalDebt) {
return `<span style='color: ${RED}'>${ARROW_UP}</span> ${technicalDebt - previousTechnicalDebt}% more than previous month`;
}
if (previousTechnicalDebt > technicalDebt) {
return `<span style='color: ${GREEN}'>${ARROW_DOWN}</span> ${previousTechnicalDebt - technicalDebt}% less than previous month`;
}
return 'Same as last month';
},
flagsCreatedTrendMessage() {
if (!previousMonth) {
return null;
}
if (flagsCreated > previousMonth.flagsCreated) {
return `<span style='color: ${GREEN}'>${ARROW_UP}</span> ${flagsCreated - previousMonth.flagsCreated} more than previous month`;
}
if (previousMonth.flagsCreated > flagsCreated) {
return `<span style='color: ${RED}'>${ARROW_DOWN}</span> ${previousMonth.flagsCreated - flagsCreated} less than previous month`;
}
return 'Same as last month';
},
productionUpdatedTrendMessage() {
if (!previousMonth) {
return null;
}
if (productionUpdates > previousMonth.productionUpdates) {
return `<span style='color: ${GREEN}'>${ARROW_UP}</span> ${productionUpdates - previousMonth.productionUpdates} more than previous month`;
}
if (previousMonth.productionUpdates > productionUpdates) {
return `<span style='color: ${RED}'>${ARROW_DOWN}</span> ${previousMonth.productionUpdates - productionUpdates} less than previous month`;
}
return 'Same as last month';
},
};
};

View File

@ -146,7 +146,7 @@ test('Can send productivity report email', async () => {
expect(content.html).toContain('localhost/profile'); expect(content.html).toContain('localhost/profile');
expect(content.html).toContain('#68a611'); expect(content.html).toContain('#68a611');
expect(content.html).toContain('1%'); expect(content.html).toContain('1%');
expect(content.html).toContain('10% more than previous month'); expect(content.html).toContain('10% less than previous month');
expect(content.text).toContain('localhost/insights'); expect(content.text).toContain('localhost/insights');
expect(content.text).toContain('localhost/profile'); expect(content.text).toContain('localhost/profile');
expect(content.text).toContain('localhost/profile'); expect(content.text).toContain('localhost/profile');

View File

@ -27,9 +27,9 @@
<div class="shaded" <div class="shaded"
style="margin: 0;padding: 24px 8px 24px 8px; background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;"> style="margin: 0;padding: 24px 8px 24px 8px; background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
<div style="padding-top: 12px;"> <div style="padding-top: 12px;">
<span style="color: {{healthColor}};">{{technicalDebt}}%</span><br> <span style="color: {{technicalDebtColor}};">{{technicalDebt}}%</span><br>
<span style="font-size: 16px; color: #1A4049; font-weight: 700">your instance technical debt</span><br> <span style="font-size: 16px; color: #1A4049; font-weight: 700">your instance technical debt</span><br>
<span style="font-size: 12px; color: #6E6E70; font-weight: 400; line-height: 14px">{{{healthTrendMessage}}}</span> <span style="font-size: 12px; color: #6E6E70; font-weight: 400; line-height: 14px">{{{technicalDebtTrendMessage}}}</span>
<div style="font-size: 12px; margin-top: 16px; font-weight: 400; line-height: 14px;">{{actionText}}</div> <div style="font-size: 12px; margin-top: 16px; font-weight: 400; line-height: 14px;">{{actionText}}</div>
</div> </div>
</div> </div>
@ -75,8 +75,8 @@
style="color: #1a4049;margin: 0 10px;text-decoration: none;">getunleash.io</a> | style="color: #1a4049;margin: 0 10px;text-decoration: none;">getunleash.io</a> |
<a href="https://github.com/unleash" <a href="https://github.com/unleash"
style="color: #1a4049;margin: 0 10px;text-decoration: none;">GitHub</a> | style="color: #1a4049;margin: 0 10px;text-decoration: none;">GitHub</a> |
<a href="https://twitter.com/unleash" <a href="https://www.linkedin.com/company/getunleash/"
style="color: #1a4049;margin: 0 10px;text-decoration: none;">Twitter</a> style="color: #1a4049;margin: 0 10px;text-decoration: none;">LinkedIn</a>
</div> </div>
<div class="unsubscribe" style="font-size: 12px;color: #888;margin-top: 10px;"> <div class="unsubscribe" style="font-size: 12px;color: #888;margin-top: 10px;">
This email was sent to {{userEmail}}. Youve received this as you are a user of Unleash.<br> This email was sent to {{userEmail}}. Youve received this as you are a user of Unleash.<br>