Skip to content
This repository has been archived by the owner on May 31, 2024. It is now read-only.

Commit

Permalink
feat(matomo): add monthly download stats (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
cberthou authored Mar 5, 2021
1 parent 3c3aa43 commit 8673421
Show file tree
Hide file tree
Showing 13 changed files with 158 additions and 35 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@types/jest": "^26.0.20",
"axios": "^0.21.0",
"cors": "^2.8.5",
"date-fns": "^2.18.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"lodash": "^4.17.20",
Expand Down
6 changes: 2 additions & 4 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import { flatten } from "lodash/fp";

import packageJson from "../package.json";
import { createCache } from "./caching/caching-service";
import { corsOrigins, port } from "./config";
import { cacheTTL, corsOrigins, port } from "./config";
import { getGitHubData } from "./github/github-service";
import { matomoConfig } from "./matomo/matomo-config";
import { getMultiSiteMatomoData } from "./matomo/matomo-service";
import { getYoutubeData } from "./youtube/youtube-service";

const CACHE_TTL = 10 * 60 * 1000;

const app = express();

app.use(
Expand All @@ -35,7 +33,7 @@ const statsCache = createCache(
getYoutubeData(),
getGitHubData(),
]).then(flatten),
CACHE_TTL
cacheTTL
);

app.get("/statistics", (req, res) => {
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export const youtubeChannelId: string = process.env.YOUTUBE_CHANNEL_ID ?? "";
export const githubApiUrl: string = process.env.GITHUB_API_URL ?? "";
export const githubApiKey: string = process.env.GITHUB_API_KEY ?? "";
export const corsOrigins: string = process.env.CORS_ORIGINS ?? "";
export const cacheTTL: number =
parseInt(process.env.CACHE_TTL ?? "") || 10 * 60 * 1000;
2 changes: 1 addition & 1 deletion src/github/github-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ export const getGitHubData = async (): Promise<ArchifiltreCountStatistic[]> =>
.then(filterWikiItem)
.then(convertGitHubDataToApiData)
.catch((err) => {
console.log(err);
console.error(err);
return [];
});
1 change: 1 addition & 0 deletions src/matomo/matomo-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const matomoConfig: MatomoSiteConfig[] = [
],
events: ["download", "appDownload"],
idSite: 20,
monthlyEvents: ["download", "appDownload"],
visits: true,
},
];
44 changes: 29 additions & 15 deletions src/matomo/matomo-service.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
import axios from "axios";
import { flatten } from "lodash/fp";
import { compose, flatten } from "lodash/fp";
import querystring from "querystring";

import type { ArchifiltreCountStatistic } from "../api-types";
import { matomoToken, matomoUrl } from "../config";
import { liftPromise } from "../utils/fp-util";
import type { MatomoEventCategory, MatomoSiteConfig } from "./matomo-types";
import {
createMatomoDataSanitizer,
getBulkRequestParamsFromConfig,
} from "./matomo-utils";

const getBulkMatomoData = async (
config: MatomoSiteConfig
): Promise<MatomoEventCategory[][]> =>
axios
.get(matomoUrl, {
params: {
format: "JSON",
method: "API.getBulkRequest",
module: "API",
// eslint-disable-next-line @typescript-eslint/naming-convention
token_auth: matomoToken,
...getBulkRequestParamsFromConfig(config),
},
type BulkRequestData = {
data: MatomoEventCategory[][];
};

const makeBulkRequest = async (
params: Record<string, string>
): Promise<BulkRequestData> =>
axios.post(
matomoUrl,
querystring.stringify({
format: "JSON",
method: "API.getBulkRequest",
module: "API",
// eslint-disable-next-line @typescript-eslint/naming-convention
token_auth: matomoToken,
...params,
})
.then(({ data }: { data: MatomoEventCategory[][] }) => data);
);

const formatResult = ({ data }: BulkRequestData): MatomoEventCategory[][] =>
data;

const getBulkMatomoData = compose(
liftPromise(formatResult),
makeBulkRequest,
getBulkRequestParamsFromConfig
);

export const getMatomoData = async (
config: MatomoSiteConfig
Expand Down
1 change: 1 addition & 0 deletions src/matomo/matomo-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ export type MatomoSiteConfig = {
idSite: number;
events?: MatomoEventConfig[];
actions?: MatomoActionConfigObject[];
monthlyEvents?: MatomoEventConfig[];
visits?: boolean;
};
88 changes: 73 additions & 15 deletions src/matomo/matomo-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { flatten } from "lodash";
import { format, parseISO } from "date-fns/fp";
import { isString } from "lodash";
import { compose, map } from "lodash/fp";
import * as querystring from "querystring";

import type { ArchifiltreCountStatistic } from "../api-types";
import { getLastMonthsRanges } from "../utils/date";
import type {
MatomoActionConfigObject,
MatomoEventCategory,
Expand All @@ -10,9 +13,12 @@ import type {
MatomoSiteConfig,
} from "./matomo-types";

const MONTHS_REQUESTED = 12;

type CreateMatomoEventCategoryMethodParams = {
config: MatomoEventConfig;
idSite: number;
date?: [string, string];
};

const sanitizeMatomoEventConfig = (
Expand All @@ -25,23 +31,30 @@ const sanitizeMatomoEventConfig = (
: config;

const createMatomoRequestBaseParams = (
idSite: number
idSite: number,
date: [string, string] = ["2020-01-01", "today"]
): Record<string, number | string> => ({
date: "2020-01-01,today",
date: date.join(","),
idSite,
period: "range",
});

const createMatomoEventCategoryMethod = ({
config,
idSite,
date,
}: CreateMatomoEventCategoryMethodParams) => {
const { label } = sanitizeMatomoEventConfig(config);
return querystring.stringify({
...createMatomoRequestBaseParams(idSite),
label,
method: "Events.getCategory",
});
return querystring.stringify(
{
...createMatomoRequestBaseParams(idSite, date),
label,
method: "Events.getCategory",
},
"&",
"=",
{ encodeURIComponent: (val) => val }
);
};

type CreateMatomoEventActionMethodParams = {
Expand All @@ -67,9 +80,25 @@ const createMatomoVisitMethod = (idSite: number): string =>

type RequestParams = Record<string, string>;

const getMatomoLastMonthsRange = getLastMonthsRanges(MONTHS_REQUESTED);

type CreateMonthlyEvenMethodParams = {
config: MatomoEventConfig;
idSite: number;
};

const createMonthlyEventMethod = ({
config,
idSite,
}: CreateMonthlyEvenMethodParams) =>
getMatomoLastMonthsRange(new Date()).map((dateRange) =>
createMatomoEventCategoryMethod({ config, date: dateRange, idSite })
);

export const getBulkRequestParamsFromConfig = ({
events = [],
actions = [],
monthlyEvents = [],
visits = false,
idSite,
}: MatomoSiteConfig): RequestParams =>
Expand All @@ -80,6 +109,9 @@ export const getBulkRequestParamsFromConfig = ({
...actions.map((config) =>
createMatomoEventActionMethod({ config, idSite })
),
...monthlyEvents.flatMap((config) =>
createMonthlyEventMethod({ config, idSite })
),
...(visits ? [createMatomoVisitMethod(idSite)] : []),
].reduce(
(urlParams, urlParam, index) => ({
Expand All @@ -95,22 +127,48 @@ const formatEventsOrActionsResponse = () => (
// eslint-disable-next-line @typescript-eslint/naming-convention
eventCategories.map(({ label, nb_events }) => ({ label, value: nb_events }));

const getConfigLabel = (config: MatomoEventConfig) =>
isString(config) ? config : config.label;

type ResultFormatter = (
eventCategories: MatomoEventCategory[]
) => ArchifiltreCountStatistic[];

const formatResultDate = compose(format("y-MM"), parseISO);

const formatMonthlyApiResult = (config: MatomoEventConfig, date: string) => ({
value,
}: ArchifiltreCountStatistic) => ({
label: `${getConfigLabel(config)}:${formatResultDate(date)}`,
value,
});

const formatMonthlyEvents = (config: MatomoEventConfig): ResultFormatter[] =>
getMatomoLastMonthsRange(new Date())
.map((date): [string, ResultFormatter] => [
date[0],
formatEventsOrActionsResponse(),
])
.map(([date, formatter]) =>
compose(map(formatMonthlyApiResult(config, date)), formatter)
);

const formatVisitsResponse = () => (
value: number
): ArchifiltreCountStatistic => ({ label: "visitsCount", value });

export const createMatomoDataSanitizer = ({
events = [],
actions = [],
monthlyEvents = [],
visits = false,
}: MatomoSiteConfig) => (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
matomoApiResponse: any[]
): ArchifiltreCountStatistic[] =>
flatten(
[
...events.map(formatEventsOrActionsResponse),
...actions.map(formatEventsOrActionsResponse),
...(visits ? [formatVisitsResponse()] : []),
].map((formatter, index) => formatter(matomoApiResponse[index]))
);
[
...events.map(formatEventsOrActionsResponse),
...actions.map(formatEventsOrActionsResponse),
...monthlyEvents.flatMap(formatMonthlyEvents),
...(visits ? [formatVisitsResponse()] : []),
].flatMap((formatter, index) => formatter(matomoApiResponse[index]));
16 changes: 16 additions & 0 deletions src/utils/date.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { parseISO } from "date-fns";

import { getLastMonthsRanges } from "./date";

describe("date", () => {
describe("getLastMonthsRanges", () => {
it("should compute the last months ranges", () => {
const startDate = parseISO("2021-02-10");
expect(getLastMonthsRanges(3)(startDate)).toEqual([
["2021-02-01", "2021-02-10"],
["2021-01-01", "2021-01-31"],
["2020-12-01", "2020-12-31"],
]);
});
});
});
19 changes: 19 additions & 0 deletions src/utils/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { endOfMonth, formatISO, startOfMonth, subMonths } from "date-fns";
import { range } from "lodash";

export const getLastMonthsRanges = (monthCount: number) => (
now: Date
): [string, string][] =>
range(monthCount)
.map((index) => subMonths(now, index))
.map((date, index): [Date, Date] => [
startOfMonth(date),
index === 0 ? date : endOfMonth(date),
])
.map(
(dates): [string, string] =>
dates.map((date) => formatISO(date, { representation: "date" })) as [
string,
string
]
);
7 changes: 7 additions & 0 deletions src/utils/fp-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Lift method for promise
* @param method
*/
export const liftPromise = <TInput, TOutput>(
method: (input: TInput) => TOutput
) => async (promise: Promise<TInput>): Promise<TOutput> => promise.then(method);
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es2019"],
"module": "commonjs",
"allowJs": false,
"outDir": "./dist",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"

date-fns@^2.18.0:
version "2.18.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.18.0.tgz#08e50aee300ad0d2c5e054e3f0d10d8f9cdfe09e"
integrity sha512-NYyAg4wRmGVU4miKq5ivRACOODdZRY3q5WLmOJSq8djyzftYphU7dTHLcEtLqEvfqMKQ0jVv91P4BAwIjsXIcw==

debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
Expand Down

0 comments on commit 8673421

Please sign in to comment.