Skip to content

Commit

Permalink
Merge pull request #236 from Pinelab-studio/feat/metric-custom-strategy
Browse files Browse the repository at this point in the history
Metrics: Reintroduce custom strategies to display custom metrics in dashboard
  • Loading branch information
martijnvdbrug authored Sep 7, 2023
2 parents e492d19 + d6fe8d1 commit f7fa659
Show file tree
Hide file tree
Showing 24 changed files with 1,674 additions and 1,024 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@commitlint/cli": "17.2.0",
"@commitlint/config-conventional": "17.2.0",
"@google-cloud/storage": "5.18.2",
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript-document-nodes": "2.2.8",
"@graphql-codegen/typescript-operations": "2.3.5",
Expand All @@ -44,12 +44,12 @@
"@vendure/ui-devkit": "2.0.6",
"aws-sdk": "2.1099.0",
"copyfiles": "2.4.1",
"eslint": "8.0.1",
"eslint-config-prettier": "8.8.0",
"eslint-config-standard-with-typescript": "34.0.1",
"eslint-plugin-import": "2.25.2",
"eslint-plugin-n": "15.0.0",
"eslint-plugin-promise": "6.0.0",
"eslint": "8.0.1",
"graphql-tag": "2.12.6",
"husky": "8.0.2",
"lerna": "6.0.3",
Expand Down
4 changes: 3 additions & 1 deletion packages/vendure-plugin-metrics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
// TODO set correct version number + date and the changes you've made connected to the PR. See this example for the correct format: https://github.com/Pinelab-studio/pinelab-vendure-plugins/blob/main/packages/vendure-plugin-invoices/CHANGELOG.md
# 1.1.0 (2023-09-076)

- Reintroduced custom strategies and using the new Chartist charts ([#236](https://github.com/Pinelab-studio/pinelab-vendure-plugins/pull/236))
148 changes: 120 additions & 28 deletions packages/vendure-plugin-metrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,147 @@ month or per week and number of items per product variant for the past 12 months
1. Configure the plugin in `vendure-config.ts`:

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

plugins: [
...
MetricsPlugin,
MetricsPlugin.init({
metrics: [
new AverageOrderValueMetric(),
new SalesPerProductMetric()
]
}),
AdminUiPlugin.init({
port: 3002,
route: 'admin',
app: compileUiExtensions({
outputPath: path.join(__dirname, '__admin-ui'),
extensions: [SalesPerVariantPlugin.ui],
extensions: [MetricsPlugin.ui],
}),
}),
...
]
```

2. Start your Vendure server and login as administrator
3. You should now be able to select `metrics` when you click on the button `add widget`
3. 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.

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

1. Average Order Value (AOV): The average of `order.totalWithTax` of the orders per week/month
2. Nr of items: The number of items sold. When no variants are selected, this metric counts the total nr of items in an order.
3. Nr of orders: The number of order per week/month

# Breaking changes since 5.x

For simplicity and performance reasons, we decided it makes more sense to display our 3 metrics of choice, and not have metrics extensible with custom Metrics for now. This is what changes in your `vendure-config`:

```diff
- plugins: [
- MetricsPlugin.init({
- metrics: [
- new NrOfOrdersMetric(),
- new AverageOrderValueMetric(),
- new ConversionRateMetric(),
- new RevenueMetric(),
- ],
- }),
- ]
+ plugins: [
+ MetricsPlugin,
+ ]
```
2. Sales per product: The number of items sold. When no variants are selected, this metric counts the total nr of items per order.

# Custom Metrics

You can now also view metrics per variant(s) if you'd like.
You can implement the `MetricStrategy` interface and pass it to the `MetricsPlugin.init()` function to have your custom metric visible in the Widget.

```ts
// Fictional example that displays the average value per order line per month in a chart

import {
Injector,
OrderLine,
ProductVariant,
RequestContext,
TransactionalConnection,
} from '@vendure/core';
import {
MetricStrategy,
NamedDatapoint,
AdvancedMetricType,
} from '@pinelab/vendure-plugin-metrics';

export class AverageOrderLineValue implements MetricStrategy<OrderLine> {
readonly metricType: AdvancedMetricType = AdvancedMetricType.Currency;
readonly code = 'average-orderline-value';

getTitle(ctx: RequestContext): string {
return `Average Order Line Value`;
}

getSortableField(entity: OrderLine): Date {
return entity.order.orderPlacedAt ?? entity.order.updatedAt;
}

// Here you fetch your order lines
async loadEntities(
ctx: RequestContext,
injector: Injector,
from: Date,
to: Date,
variants: ProductVariant[]
): Promise<OrderLine[]> {
let skip = 0;
const take = 1000;
let hasMoreOrderLines = true;
const lines: OrderLine[] = [];
while (hasMoreOrderLines) {
let query = injector
.get(TransactionalConnection)
.getRepository(ctx, OrderLine)
.createQueryBuilder('orderLine')
.leftJoin('orderLine.productVariant', 'productVariant')
.addSelect(['productVariant.sku', 'productVariant.id'])
.leftJoinAndSelect('orderLine.order', 'order')
.leftJoin('order.channels', 'channel')
.where(`channel.id=:channelId`, { channelId: ctx.channelId })
.andWhere(`order.orderPlacedAt >= :from`, {
from: from.toISOString(),
})
.andWhere(`order.orderPlacedAt <= :to`, {
to: to.toISOString(),
})
.skip(skip)
.take(take);
if (variants.length) {
query = query.andWhere(`productVariant.id IN(:...variantIds)`, {
variantIds: variants.map((v) => v.id),
});
}
const [items, totalItems] = await query.getManyAndCount();
lines.push(...items);
skip += items.length;
if (lines.length >= totalItems) {
hasMoreOrderLines = false;
}
}
return lines;
}

// This is where you return the actual data points
calculateDataPoints(
ctx: RequestContext,
lines: OrderLine[],
// Variants are given when a user is filtering based on variants in the chart widget
variants: ProductVariant[]
): NamedDatapoint[] {
const legendLabel = variants.length
? `Order lines with ${variants.map((v) => v.name).join(', ')}`
: 'Average order line value';
if (!lines.length) {
// Return 0 as average if no order lines
return [
{
legendLabel,
value: 0,
},
];
}
const total = lines
.map((l) => l.linePriceWithTax)
.reduce((total, current) => total + current, 0);
const average = Math.round(total / lines.length) / 100;
return [
{
legendLabel,
value: average,
},
];
}
}
```

### Contributions

Expand Down
3 changes: 2 additions & 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.0.2",
"version": "1.1.0",
"description": "Vendure plugin measuring and visualizing e-commerce metrics",
"author": "Martijn van de Brug <[email protected]>",
"homepage": "https://pinelab-plugins.com/",
Expand All @@ -24,6 +24,7 @@
"generate": "graphql-codegen"
},
"dependencies": {
"chartist-plugin-tooltips-updated": "^1.0.0",
"date-fns": "^2.29.3"
},
"gitHead": "476f36da3aafea41fbf21c70774a30306f1d238f"
Expand Down
73 changes: 73 additions & 0 deletions packages/vendure-plugin-metrics/src/api/metric-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
AdvancedMetricSummaryInput,
AdvancedMetricType,
} from '../ui/generated/graphql';
import { RequestContext, Injector, ProductVariant } from '@vendure/core';
/**
* GroupId is used to group datapoints. For example 'product1', so that the plugin can find all datapoints for that product;
*/
export interface NamedDatapoint {
legendLabel: string;
value: number;
}

export interface MetricStrategy<T> {
code: string;
/**
* We need to know if the chart should format your metrics as
* numbers/amounts or as currency
*/
metricType: AdvancedMetricType;

/**
* Title to display on the chart.
* Ctx can be used to localize the title
*/
getTitle(ctx: RequestContext): string;

/**
* Should return the date to sort by. This value is used to determine in what month the datapoint should be displayed.
* For example `order.orderPlacedAt` when you are doing metrics for Orders.
* By default `creeatedAt` is used
*/
getSortableField?(entity: T): Date;

/**
* Load your entities for the given time frame here.
* A client can optionally supply variants as input, which means metrics should be shown for the selected variants only
*
* Keep performance and object size in mind:
*
* Entities are cached in memory, so only return data you actually use in your calculateDataPoint function
*
* This function is executed in the main thread when a user views its dashboard,
* so try not to fetch objects with many relations
*/
loadEntities(
ctx: RequestContext,
injector: Injector,
from: Date,
to: Date,
variants: ProductVariant[]
): Promise<T[]>;

/**
* Calculate the aggregated datapoint for the given data.
* E.g. the sum of all given data, or the average.
*
* Return multiple datapoints for a multi line chart.
* The name will be used as legend on the chart.
*
* @example
* // Number of products sold
* [
* {name: 'product1', value: 10 },
* {name: 'product2', value: 16 }
* ]
*/
calculateDataPoints(
ctx: RequestContext,
entities: T[],
variants: ProductVariant[]
): NamedDatapoint[];
}
9 changes: 5 additions & 4 deletions packages/vendure-plugin-metrics/src/api/metrics.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { Args, Query, Resolver } from '@nestjs/graphql';
import { Allow, Ctx, Permission, RequestContext } from '@vendure/core';
import { MetricsService } from './metrics.service';
import {
AdvancedMetricSummary,
AdvancedMetricSummaryInput,
AdvancedMetricType,
} from '../ui/generated/graphql';
import { MetricsService } from './metrics.service';

@Resolver()
export class MetricsResolver {
constructor(private service: MetricsService) {}
constructor(private readonly metricsService: MetricsService) {}

@Query()
@Allow(Permission.ReadOrder)
async advancedMetricSummary(
async advancedMetricSummaries(
@Ctx() ctx: RequestContext,
@Args('input') input: AdvancedMetricSummaryInput
): Promise<AdvancedMetricSummary[]> {
return this.service.getMetrics(ctx, input);
return this.metricsService.getMetrics(ctx, input);
}
}
Loading

0 comments on commit f7fa659

Please sign in to comment.