Skip to content

Commit

Permalink
Merge pull request #537 from Pinelab-studio/feat/metrics-no-conversion
Browse files Browse the repository at this point in the history
Feat/metrics : removed conversion, added date display option
  • Loading branch information
martijnvdbrug authored Nov 19, 2024
2 parents 661d9b1 + f6a34d8 commit 05022f0
Show file tree
Hide file tree
Showing 11 changed files with 75 additions and 155 deletions.
Binary file added docs-website/public/plugin-images/metrics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/vendure-plugin-metrics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 1.6.0 (2024-11-19)

- Allow showing metrics of past X months instead of always past 12 months.
- Removed conversion metric, as it is can not be accurately calculated based on created orders alone
- Small improvement in query performance

# 1.5.0 (2024-11-03)

- Allow specifying per metric if it's filterable by variant ID's
Expand Down
29 changes: 16 additions & 13 deletions packages/vendure-plugin-metrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,22 @@

### [Official documentation here](https://pinelab-plugins.com/plugin/vendure-plugin-metrics)

A plugin to measure and visualize your shop's average order value (AOV),number of orders per
month or per week and number of items per product variant for the past 12 months (or weeks) per variants.
A plugin to visualize your shops most important metrics of the past year.

![image](https://user-images.githubusercontent.com/6604455/236404288-e55c37ba-9508-43e6-a54c-2eb7b3cd36ee.png)
![image](https://raw.githubusercontent.com/Pinelab-studio/pinelab-vendure-plugins/96ed9d15e7a2908e0620a8a1e92b1d8c9fe381a4/docs-website/public/plugin-images/metrics.png)

## Getting started

1. Configure the plugin in `vendure-config.ts`:

```ts
import { MetricsPlugin, AverageOrderValueMetric, SalesPerProductMetric } from "@pinelab/vendure-plugin-metrics";
import { MetricsPlugin } from "@pinelab/vendure-plugin-metrics";

plugins: [
...
MetricsPlugin.init({
metrics: [
new AverageOrderValueMetric(),
new SalesPerProductMetric()
]
// Consider displaying fewer months for shops with a lot of orders
displayPastMonths: 13
}),
AdminUiPlugin.init({
port: 3002,
Expand All @@ -34,15 +31,17 @@ plugins: [
]
```

2. Start your Vendure server and login as administrator
3. You should now be able to add the widget `metrics` on your dashboard.
2. Rebuild your Admin UI
3. Start your Vendure server and login as administrator
4. You should now be able to add the widget `metrics` on your dashboard.

Metric results are cached in memory to prevent heavy database queries every time a user opens its dashboard.

### Default built-in Metrics
### Built-in Metrics

1. Average Order Value (AOV): The average of `order.totalWithTax` of the orders per week/month
2. Sales per product: The number of items sold. When no variants are selected, this metric counts the total nr of items per order.
1. Revenue (per product): The total revenue per month, or the revenue generated by specific variants if a variant is selected.
2. Average Order Value (AOV): The average of `order.totalWithTax` of the orders per week/month
3. Units sold: The number of units sold for the selected variant(s).

# Custom Metrics

Expand All @@ -67,6 +66,10 @@ import {
export class AverageOrderLineValue implements MetricStrategy<OrderLine> {
readonly metricType: AdvancedMetricType = AdvancedMetricType.Currency;
readonly code = 'average-orderline-value';
/**
* Determines if this metric allows filtering by variants
*/
readonly allowProductSelection = true;

getTitle(ctx: RequestContext): string {
return `Average Order Line Value`;
Expand Down
2 changes: 1 addition & 1 deletion packages/vendure-plugin-metrics/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pinelab/vendure-plugin-metrics",
"version": "1.5.0",
"version": "1.6.0",
"description": "Vendure plugin measuring and visualizing e-commerce metrics",
"author": "Martijn van de Brug <[email protected]>",
"homepage": "https://pinelab-plugins.com/",
Expand Down
14 changes: 8 additions & 6 deletions packages/vendure-plugin-metrics/src/api/metrics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,18 @@ export class MetricsService {
);
const today = endOfDay(new Date());
// Use start of month, because we'd like to see the full results of last years same month
const oneYearAgo = startOfMonth(sub(today, { years: 1 }));
const startDate = startOfMonth(
sub(today, { months: this.pluginOptions.displayPastMonths })
);
// For each metric strategy
return Promise.all(
this.metricStrategies.map(async (metricStrategy) => {
const cacheKeyObject = {
code: metricStrategy.code,
from: today.toDateString(),
to: oneYearAgo.toDateString(),
from: startDate.toDateString(),
to: today.toDateString(),
channel: ctx.channel.token,
variantIds: [] as string[],
variantIds: [] as string[], // Set below if input is given
};
if (metricStrategy.allowProductSelection) {
// Only use variantIds for cache key if the strategy allows filtering by variants
Expand All @@ -78,14 +80,14 @@ export class MetricsService {
const allEntities = await metricStrategy.loadEntities(
ctx,
new Injector(this.moduleRef),
oneYearAgo,
startDate,
today,
variants
);
const entitiesPerMonth = this.splitEntitiesInMonths(
metricStrategy,
allEntities,
oneYearAgo,
startDate,
today
);
// Calculate datapoints per 'name', because we could be dealing with a multi line chart
Expand Down
101 changes: 0 additions & 101 deletions packages/vendure-plugin-metrics/src/api/metrics/conversion.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ export class UnitsSoldMetric implements MetricStrategy<OrderLine> {
.getRepository(ctx, OrderLine)
.createQueryBuilder('orderLine')
.leftJoin('orderLine.order', 'order')
.addSelect(['order.id', 'order.orderPlacedAt'])
.select([
'order.id',
'order.orderPlacedAt',
'orderLine.id',
'orderLine.quantity',
])
.leftJoin('order.channels', 'channel')
.where(`channel.id=:channelId`, { channelId: ctx.channelId })
.andWhere('order.orderPlacedAt BETWEEN :fromDate AND :toDate', {
Expand Down
1 change: 0 additions & 1 deletion packages/vendure-plugin-metrics/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@ export * from './ui/generated/graphql';
export * from './api/metrics/average-order-value';
export * from './api/metrics/revenue-per-product';
export * from './api/metrics/units-sold-metric';
export * from './api/metrics/conversion';
18 changes: 14 additions & 4 deletions packages/vendure-plugin-metrics/src/metrics.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@ import { PLUGIN_INIT_OPTIONS } from './constants';
import { RevenuePerProduct } from './api/metrics/revenue-per-product';
import { AverageOrderValueMetric } from './api/metrics/average-order-value';
import { UnitsSoldMetric } from './api/metrics/units-sold-metric';
import { ConversionMetric } from './api/metrics/conversion';

export interface MetricsPluginOptions {
/**
* The enabled metrics shown in the widget.
*/
metrics: MetricStrategy<any>[];
/**
* The number of past months to display in the metrics widget.
* If your shop has a lot of orders, consider using only the last 3 months for example.
*/
displayPastMonths: number;
}

@VendurePlugin({
Expand All @@ -31,14 +38,17 @@ export class MetricsPlugin {
static options: MetricsPluginOptions = {
metrics: [
new RevenuePerProduct(),
new ConversionMetric(),
new AverageOrderValueMetric(),
new UnitsSoldMetric(),
],
displayPastMonths: 13,
};

static init(options: MetricsPluginOptions): typeof MetricsPlugin {
this.options = options;
static init(options: Partial<MetricsPluginOptions>): typeof MetricsPlugin {
this.options = {
...this.options,
...options,
};
return MetricsPlugin;
}

Expand Down
4 changes: 3 additions & 1 deletion packages/vendure-plugin-metrics/test/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import { addItem, createSettledOrder } from '../../test/src/shop-utils';
paymentMethodHandlers: [testPaymentMethod],
},
plugins: [
MetricsPlugin,
MetricsPlugin.init({
displayPastMonths: 19,
}),
DefaultSearchPlugin,
AdminUiPlugin.init({
port: 3002,
Expand Down
48 changes: 21 additions & 27 deletions packages/vendure-plugin-metrics/test/metrics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe('Metrics', () => {
await expect(promise).rejects.toThrow('authorized');
});

it('Fetches metrics for past 13 months', async () => {
it('Fetches metrics for past 14 months', async () => {
await adminClient.asSuperAdmin();
const { advancedMetricSummaries } =
await adminClient.query<AdvancedMetricSummariesQuery>(GET_METRICS);
Expand All @@ -84,23 +84,17 @@ describe('Metrics', () => {
const salesPerProduct = advancedMetricSummaries.find(
(m) => m.code === 'units-sold'
)!;
const conversion = advancedMetricSummaries.find(
(m) => m.code === 'conversion'
)!;
expect(advancedMetricSummaries.length).toEqual(4);
expect(averageOrderValue.series[0].values.length).toEqual(13);
expect(averageOrderValue.labels.length).toEqual(13);
expect(advancedMetricSummaries.length).toEqual(3);
expect(averageOrderValue.series[0].values.length).toEqual(14);
expect(averageOrderValue.labels.length).toEqual(14);
// All orders are 4102 without tax, so that the AOV
expect(averageOrderValue.series[0].values[12]).toEqual(4102);
expect(revenuePerProduct.series[0].values.length).toEqual(13);
expect(revenuePerProduct.labels.length).toEqual(13);
expect(averageOrderValue.series[0].values[13]).toEqual(4102);
expect(revenuePerProduct.series[0].values.length).toEqual(14);
expect(revenuePerProduct.labels.length).toEqual(14);
// All orders are 4102 without tax, and we placed 3 orders
expect(revenuePerProduct.series[0].values[12]).toEqual(3 * 4102); //12306
expect(salesPerProduct.series[0].values.length).toEqual(13);
expect(salesPerProduct.labels.length).toEqual(13);
expect(conversion.series[0].values.length).toEqual(13);
expect(conversion.labels.length).toEqual(13);
expect(conversion.series[0].values[12]).toEqual(100);
expect(revenuePerProduct.series[0].values[13]).toEqual(3 * 4102); //12306
expect(salesPerProduct.series[0].values.length).toEqual(14);
expect(salesPerProduct.labels.length).toEqual(14);
});

it('Fetches metrics for specific variant', async () => {
Expand All @@ -109,29 +103,29 @@ describe('Metrics', () => {
await adminClient.query<AdvancedMetricSummariesQuery>(GET_METRICS, {
input: { variantIds: [1, 2] },
});
expect(advancedMetricSummaries.length).toEqual(4);
expect(advancedMetricSummaries.length).toEqual(3);
const revenuePerProduct = advancedMetricSummaries.find(
(m) => m.code === 'revenue-per-product'
)!;
// For revenue per product we expect 2 series: one for each variant
expect(revenuePerProduct.series[0].values.length).toEqual(13);
expect(revenuePerProduct.series[1].values.length).toEqual(13);
expect(revenuePerProduct.labels.length).toEqual(13);
expect(revenuePerProduct.series[0].values.length).toEqual(14);
expect(revenuePerProduct.series[1].values.length).toEqual(14);
expect(revenuePerProduct.labels.length).toEqual(14);
// Expect the first series (variant 1), to have 389700 revenue in last month
expect(revenuePerProduct.series[0].values[12]).toEqual(3897);
expect(revenuePerProduct.series[0].values[13]).toEqual(3897);
// Expect the first series (variant 2), to have 839400 revenue in last month
expect(revenuePerProduct.series[1].values[12]).toEqual(8394);
expect(revenuePerProduct.series[1].values[13]).toEqual(8394);
const salesPerProduct = advancedMetricSummaries.find(
(m) => m.code === 'units-sold'
)!;
// For sales per product we expect 2 series: one for each variant
expect(salesPerProduct.series[0].values.length).toEqual(13);
expect(salesPerProduct.series[1].values.length).toEqual(13);
expect(salesPerProduct.labels.length).toEqual(13);
expect(salesPerProduct.series[0].values.length).toEqual(14);
expect(salesPerProduct.series[1].values.length).toEqual(14);
expect(salesPerProduct.labels.length).toEqual(14);
// Expect the first series (variant 1), to have 3 revenue in last month
expect(salesPerProduct.series[0].values[12]).toEqual(3);
expect(salesPerProduct.series[0].values[13]).toEqual(3);
// Expect the first series (variant 2), to have 6 revenue in last month
expect(salesPerProduct.series[1].values[12]).toEqual(6);
expect(salesPerProduct.series[1].values[13]).toEqual(6);
});

if (process.env.TEST_ADMIN_UI) {
Expand Down

0 comments on commit 05022f0

Please sign in to comment.