Skip to content

Commit

Permalink
feat: productivity report trends visualization (#8956)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Dec 11, 2024
1 parent 311df82 commit 48b2159
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 29 deletions.
10 changes: 9 additions & 1 deletion src/lib/services/email-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,23 @@ test('Can send productivity report email', async () => {
flagsCreated: 1,
productionUpdates: 2,
health: 99,
previousMonth: {
health: 89,
flagsCreated: 1,
productionUpdates: 3,
},
},
);
expect(content.from).toBe('[email protected]');
expect(content.subject).toBe('Unleash - productivity report');
expect(content.html.includes(`Productivity Report`)).toBe(true);
expect(content.html.includes(`localhost/insights`)).toBe(true);
expect(content.html.includes(`localhost/profile`)).toBe(true);
expect(content.html.includes(`#b0d182`)).toBe(true);
expect(content.html.includes('#68a611')).toBe(true);
expect(content.html.includes(`10% more than previous month`)).toBe(true);
expect(content.text.includes(`localhost/insights`)).toBe(true);
expect(content.text.includes(`localhost/profile`)).toBe(true);
expect(content.text.includes(`localhost/profile`)).toBe(true);
});

test('Should add optional headers to productivity email', async () => {
Expand Down Expand Up @@ -170,6 +177,7 @@ test('Should add optional headers to productivity email', async () => {
flagsCreated: 1,
productionUpdates: 2,
health: 99,
previousMonth: null,
},
);

Expand Down
28 changes: 9 additions & 19 deletions src/lib/services/email-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { existsSync, readFileSync } from 'fs';
import type { Logger } from '../logger';
import NotFoundError from '../error/notfound-error';
import type { IUnleashConfig } from '../types/option';
import {
type ProductivityReportMetrics,
productivityReportViewModel,
} from '../../mailtemplates/productivity-report/productivity-report-view-model';

export interface IAuthOptions {
user: string;
Expand Down Expand Up @@ -460,29 +464,15 @@ export class EmailService {
async sendProductivityReportEmail(
userEmail: string,
userName: string,
metrics: {
health: number;
flagsCreated: number;
productionUpdates: number;
},
metrics: ProductivityReportMetrics,
): Promise<IEmailEnvelope> {
if (this.configured()) {
const context = {
userName,
const context = productivityReportViewModel({
metrics,
userEmail,
...metrics,
userName,
unleashUrl: this.config.server.unleashUrl,
healthColor() {
const healthRating = this.health;
const healthColor =
healthRating >= 0 && healthRating <= 24
? '#d93644'
: healthRating >= 25 && healthRating <= 74
? '#ffc46f'
: '#b0d182';
return healthColor;
},
};
});

const template = 'productivity-report';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {
productivityReportViewModel,
type ProductivityReportMetrics,
} from './productivity-report-view-model';

const mockData = {
unleashUrl: 'http://example.com',
userEmail: '[email protected]',
userName: 'Test User',
};
const mockMetrics = {
health: 0,
flagsCreated: 0,
productionUpdates: 0,
previousMonth: {
health: 0,
flagsCreated: 0,
productionUpdates: 0,
},
};

describe('productivityReportViewModel', () => {
describe('healthColor', () => {
it('returns RED for health between 0 and 24', () => {
const metrics: ProductivityReportMetrics = {
...mockMetrics,
health: 20,
};

const viewModel = productivityReportViewModel({
...mockData,
metrics,
});

expect(viewModel.healthColor()).toBe('#d93644');
});

it('returns ORANGE for health between 25 and 74', () => {
const metrics: ProductivityReportMetrics = {
...mockMetrics,
health: 50,
};

const viewModel = productivityReportViewModel({
...mockData,
metrics,
});

expect(viewModel.healthColor()).toBe('#d76500');
});

it('returns GREEN for health 75 or above', () => {
const metrics: ProductivityReportMetrics = {
...mockMetrics,
health: 80,
};

const viewModel = productivityReportViewModel({
...mockData,
metrics,
});

expect(viewModel.healthColor()).toBe('#68a611');
});
});

describe('healthTrendMessage', () => {
it('returns correct trend message when health increased', () => {
const metrics: ProductivityReportMetrics = {
...mockMetrics,
health: 80,
previousMonth: { ...mockMetrics.previousMonth, health: 70 },
};

const viewModel = productivityReportViewModel({
...mockData,
metrics,
});

expect(viewModel.healthTrendMessage()).toBe(
"<span style='color: #68a611'>&#9650;</span> 10% more than previous month",
);
});

it('returns correct trend message when health decreased', () => {
const metrics: ProductivityReportMetrics = {
...mockMetrics,
health: 60,
previousMonth: { ...mockMetrics.previousMonth, health: 70 },
};

const viewModel = productivityReportViewModel({
...mockData,
metrics,
});

expect(viewModel.healthTrendMessage()).toBe(
"<span style='color: #d93644'>&#9660;</span> 10% less than previous month",
);
});

it('returns correct message when health is the same', () => {
const metrics: ProductivityReportMetrics = {
...mockMetrics,
health: 70,
previousMonth: { ...mockMetrics.previousMonth, health: 70 },
};

const viewModel = productivityReportViewModel({
...mockData,
metrics,
});

expect(viewModel.healthTrendMessage()).toBe('Same as last month');
});
});

describe('flagsCreatedTrendMessage', () => {
it('returns correct trend message for flagsCreated increase', () => {
const metrics: ProductivityReportMetrics = {
...mockMetrics,
flagsCreated: 10,
previousMonth: {
...mockMetrics.previousMonth,
flagsCreated: 8,
},
};

const viewModel = productivityReportViewModel({
...mockData,
metrics,
});

expect(viewModel.flagsCreatedTrendMessage()).toBe(
"<span style='color: #68a611'>&#9650;</span> 2 more than previous month",
);
});
});

describe('productionUpdatedTrendMessage', () => {
it('returns correct trend message for productionUpdates decrease', () => {
const metrics: ProductivityReportMetrics = {
...mockMetrics,
productionUpdates: 5,
previousMonth: {
...mockMetrics.previousMonth,
productionUpdates: 8,
},
};

const viewModel = productivityReportViewModel({
...mockData,
metrics,
});

expect(viewModel.productionUpdatedTrendMessage()).toBe(
"<span style='color: #d93644'>&#9660;</span> 3 less than previous month",
);
});
});

describe('Missing previous month data', () => {
it('returns no trends messages', () => {
const metrics: ProductivityReportMetrics = {
health: 100,
flagsCreated: 10,
productionUpdates: 5,
previousMonth: null,
};

const viewModel = productivityReportViewModel({
...mockData,
metrics,
});

expect(viewModel.healthTrendMessage()).toBe(null);
expect(viewModel.flagsCreatedTrendMessage()).toBe(null);
expect(viewModel.productionUpdatedTrendMessage()).toBe(null);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
export type ProductivityReportMetrics = {
health: number;
flagsCreated: number;
productionUpdates: number;
previousMonth: {
health: number;
flagsCreated: number;
productionUpdates: number;
} | null;
};

const RED = '#d93644';
const GREEN = '#68a611';
const ORANGE = '#d76500';

export const productivityReportViewModel = ({
unleashUrl,
userEmail,
userName,
metrics,
}: {
unleashUrl: string;
userEmail: string;
userName: string;
metrics: ProductivityReportMetrics;
}) => ({
userName,
userEmail,
...metrics,
unleashUrl,
healthColor() {
const healthRating = this.health;
const healthColor =
healthRating >= 0 && healthRating <= 24
? RED
: healthRating >= 25 && healthRating <= 74
? ORANGE
: GREEN;
return healthColor;
},
healthTrendMessage() {
return this.previousMonthText(
'%',
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;
}
if (currentValue > previousValue) {
return `<span style='color: ${GREEN}'>&#9650;</span> ${currentValue - previousValue}${unit} more than previous month`;
}
if (previousValue > currentValue) {
return `<span style='color: ${RED}'>&#9660;</span> ${previousValue - currentValue}${unit} less than previous month`;
}
return `Same as last month`;
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
style="margin: 0;padding: 36px 8px;background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
<div style="padding-top: 12px;">
<span style="color: {{healthColor}};">{{health}}%</span><br>
<span style="font-size: 16px; color: #888;">your instance health</span>
<span style="font-size: 16px; color: #1A4049; font-weight: 700">your instance health</span><br>
<span style="font-size: 12px; color: #6E6E70; font-weight: 400; line-height: 14px">{{{healthTrendMessage}}}</span>
</div>
</div>
</div>
Expand All @@ -40,18 +41,20 @@
<tr>
<td style="text-align: center;">
<div class="shaded"
style="margin: 0;padding: 42px 8px;background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
<span
style="font-size: 24px; font-weight: bold; color: #1a4049;">{{flagsCreated}}</span><br>
<span style="color: #888;display: block;height: 20px;">flags created last month</span>
style="margin: 0;padding: 42px 8px;background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
<span
style="font-size: 24px; font-weight: bold; color: #1a4049;">{{flagsCreated}}</span><br>
<span style="color: #1A4049;display: block;height: 16px;">flags created</span><br>
<span style="font-size: 12px; color: #6E6E70; font-weight: 400;">{{{flagsCreatedTrendMessage}}}</span>
</div>
</td>
<td style="text-align: center;">
<div class="shaded"
style="margin: 0;padding: 42px 8px;background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
<span
style="font-size: 24px; font-weight: bold; color: #1a4049;">{{productionUpdates}}</span><br>
<span style="color: #888;display: block;height: 20px;">production updates last month</span>
style="margin: 0;padding: 42px 8px;background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
<span
style="font-size: 24px; font-weight: bold; color: #1a4049;">{{productionUpdates}}</span><br>
<span style="color: #1A4049;display: block;height: 16px;">production updates</span><br>
<span style="font-size: 12px; color: #6E6E70; font-weight: 400;">{{{productionUpdatedTrendMessage}}}</span>
</div>
</td>
</tr>
Expand Down

0 comments on commit 48b2159

Please sign in to comment.