@socialgouv/matomo-postgres 1.5.2-beta.1 → 1.5.2-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ process.env.MATOMO_SITE = "42";
7
+ process.env.PROJECT_NAME = "some-project";
8
+ const visit_json_1 = __importDefault(require("./visit.json"));
9
+ const importEvent_1 = require("../importEvent");
10
+ test("getEventsFromMatomoVisit: should merge action events", () => {
11
+ // @ts-ignore
12
+ const visits = (0, importEvent_1.getEventsFromMatomoVisit)(visit_json_1.default);
13
+ expect(visits).toMatchSnapshot();
14
+ });
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ const mock_pgQuery = jest.fn();
3
+ const mock_matomoApi = jest.fn();
4
+ // import formatISO from "date-fns/formatISO";
5
+ // import addDays from "date-fns/addDays";
6
+ // import { INITIAL_OFFSET } from "../config";
7
+ process.env.MATOMO_SITE = "42";
8
+ process.env.PROJECT_NAME = "some-project";
9
+ // import matomoVisit from "./visit.json";
10
+ // import run from "../index";
11
+ // const NB_REQUEST_TO_INIT_DB = 3; // Number of query to init DB (createTable.js)
12
+ // const TEST_DATE = new Date();
13
+ // @ts-ignore
14
+ //const isoDate = (date) => formatISO(date, { representation: "date" });
15
+ // jest.mock("pg", () => {
16
+ // class Client {
17
+ // escapeIdentifier(name) {
18
+ // return name;
19
+ // }
20
+ // end() {}
21
+ // connect() {
22
+ // return Promise.resolve();
23
+ // }
24
+ // query(...args) {
25
+ // return Promise.resolve(mock_pgQuery(...args));
26
+ // }
27
+ // }
28
+ // return {
29
+ // Client,
30
+ // };
31
+ // });
32
+ // jest.mock("piwik-client", () => {
33
+ // class MatomoClient {
34
+ // api(...args: any[]) {
35
+ // return mock_matomoApi(...args);
36
+ // }
37
+ // }
38
+ // return MatomoClient;
39
+ // });
40
+ test("test", () => { });
41
+ /*
42
+
43
+ beforeEach(() => {
44
+ jest.resetAllMocks();
45
+ jest.resetModules();
46
+ process.env.STARTDATE = "";
47
+ //process.env.OFFSET = "";
48
+ });
49
+
50
+ test("run: should create table", async () => {
51
+ mock_pgQuery.mockReturnValue({ rows: [] });
52
+ mock_matomoApi.mockImplementation((options, cb) => {
53
+ return cb(null, []);
54
+ });
55
+ await run();
56
+ expect(mock_pgQuery.mock.calls[0]).toMatchSnapshot();
57
+ });
58
+
59
+ test("run: should fetch the latest event date if no date provided", async () => {
60
+ jest.useFakeTimers().setSystemTime(TEST_DATE.getTime());
61
+ mock_pgQuery.mockReturnValue({ rows: [] });
62
+
63
+ mock_matomoApi.mockImplementation((options, cb) => {
64
+ return cb(null, [
65
+ {
66
+ ...matomoVisit,
67
+ idVisit: 123,
68
+ },
69
+ {
70
+ ...matomoVisit,
71
+ idVisit: 124,
72
+ },
73
+ ]);
74
+ });
75
+
76
+ await run();
77
+
78
+ // check matomo requests
79
+ expect(mock_matomoApi.mock.calls[0][0].date).toEqual(isoDate(TEST_DATE));
80
+ expect(mock_matomoApi.mock.calls[0][0].filter_offset).toEqual(0);
81
+
82
+ // check db queries
83
+ expect(mock_pgQuery.mock.calls[NB_REQUEST_TO_INIT_DB][0]).toEqual(
84
+ "select action_timestamp from matomo order by action_timestamp desc limit 1"
85
+ );
86
+ });
87
+
88
+ test("run: should resume using latest event date - offset if no date provided", async () => {
89
+ jest.useFakeTimers().setSystemTime(TEST_DATE.getTime());
90
+
91
+ const LAST_EVENT_DATE_OFFSET = 2;
92
+ // @ts-ignore
93
+ const LAST_EVENT_DATE = addDays(TEST_DATE, -LAST_EVENT_DATE_OFFSET);
94
+
95
+ mock_pgQuery.mockReturnValue({ rows: [{ action_timestamp: LAST_EVENT_DATE.getTime() }] });
96
+
97
+ mock_matomoApi.mockImplementation((options, cb) => {
98
+ return cb(null, [
99
+ {
100
+ ...matomoVisit,
101
+ idVisit: 123,
102
+ },
103
+ {
104
+ ...matomoVisit,
105
+ idVisit: 124,
106
+ },
107
+ ]);
108
+ });
109
+
110
+ await run();
111
+
112
+ // check matomo requests
113
+ expect(mock_matomoApi.mock.calls[0][0].date).toEqual(
114
+ // @ts-ignore
115
+ isoDate(addDays(LAST_EVENT_DATE, -parseInt(INITIAL_OFFSET)))
116
+ );
117
+ expect(mock_matomoApi.mock.calls[0][0].filter_offset).toEqual(0);
118
+
119
+ const daysCount = LAST_EVENT_DATE_OFFSET + parseInt(INITIAL_OFFSET) + 1;
120
+ expect(mock_matomoApi.mock.calls.length).toEqual(daysCount);
121
+
122
+ // check db queries
123
+ expect(mock_pgQuery.mock.calls.length).toEqual(NB_REQUEST_TO_INIT_DB + 1 + daysCount * 7); // NB_REQUEST_TO_INIT_DB + select queries + days offset
124
+ });
125
+
126
+ test("run: should use today date if nothing in DB", async () => {
127
+ jest.useFakeTimers().setSystemTime(TEST_DATE.getTime());
128
+ mock_pgQuery.mockReturnValue({ rows: [] });
129
+
130
+ mock_matomoApi.mockImplementation((options, cb) => {
131
+ return cb(null, [
132
+ {
133
+ ...matomoVisit,
134
+ idVisit: 123,
135
+ },
136
+ ]);
137
+ });
138
+
139
+ await run();
140
+
141
+ // check matomo requests
142
+ expect(mock_matomoApi.mock.calls.length).toEqual(1);
143
+ expect(mock_matomoApi.mock.calls[0][0].date).toEqual(isoDate(TEST_DATE));
144
+
145
+ // check the 4 events inserted
146
+ expect(mock_pgQuery.mock.calls.length).toEqual(NB_REQUEST_TO_INIT_DB + 5); // NB_REQUEST_TO_INIT_DB + check date + latest + 2 inserts
147
+ });
148
+
149
+ test("run: should use given date if any", async () => {
150
+ jest.useFakeTimers().setSystemTime(TEST_DATE.getTime());
151
+ mock_pgQuery.mockReturnValue({ rows: [] });
152
+
153
+ mock_matomoApi.mockImplementation((options, cb) => {
154
+ return cb(null, [
155
+ {
156
+ ...matomoVisit,
157
+ idVisit: 123,
158
+ },
159
+ ]);
160
+ });
161
+
162
+ // @ts-ignore
163
+ await run(isoDate(addDays(TEST_DATE, -10)) + "T00:00:00.000Z");
164
+
165
+ expect(mock_matomoApi.mock.calls.length).toEqual(11);
166
+ expect(mock_pgQuery.mock.calls.length).toEqual(NB_REQUEST_TO_INIT_DB + 11 * 4); // NB_REQUEST_TO_INIT_DB + inserts. no initial select as date is provided
167
+ });
168
+
169
+ test("run: should use STARTDATE if any", async () => {
170
+ // @ts-ignore
171
+ process.env.STARTDATE = isoDate(addDays(TEST_DATE, -5)) + "T00:00:00.000Z";
172
+ jest.useFakeTimers().setSystemTime(TEST_DATE.getTime());
173
+ mock_pgQuery.mockReturnValue({ rows: [] });
174
+
175
+ mock_matomoApi.mockImplementation((options, cb) => {
176
+ return cb(null, [
177
+ {
178
+ ...matomoVisit,
179
+ idVisit: 123,
180
+ },
181
+ ]);
182
+ });
183
+
184
+ await run();
185
+
186
+ expect(mock_matomoApi.mock.calls.length).toEqual(6);
187
+
188
+ expect(mock_pgQuery.mock.calls.length).toEqual(NB_REQUEST_TO_INIT_DB + 1 + 6 * 4); // NB_REQUEST_TO_INIT_DB + initial select + inserts.
189
+ });
190
+ */
@@ -0,0 +1,103 @@
1
+ {
2
+ "idVisit": "124",
3
+ "idSite": "42",
4
+ "country": "Argentine",
5
+ "operatingSystemName": "Mac",
6
+ "deviceBrand": "Inconnu",
7
+ "deviceModel": "Générique Bureau",
8
+ "visitDuration": "300",
9
+ "daysSinceFirstVisit": "23",
10
+ "actions": "2",
11
+ "visitorType": "returningCustomer",
12
+ "visitorId": "visitorId",
13
+ "referrerType": "referrerType",
14
+ "referrerName": "referrerName",
15
+ "siteName": "tests",
16
+ "userId": "24",
17
+ "region": "Buenos Aires",
18
+ "city": "Buenos Aires",
19
+ "dimension1": "guest",
20
+ "dimension3": "page",
21
+ "dimension6": "shop",
22
+ "dimension7": "v1.2.3",
23
+ "dimension8": "fr",
24
+ "dimension9": "light",
25
+ "dimension10": "36",
26
+ "firstActionTimestamp": 1629496512,
27
+ "actionDetails": [
28
+ {
29
+ "type": "event",
30
+ "url": "https://dive-shop.net/products/basic-wetsuit/",
31
+ "pageIdAction": "304",
32
+ "idpageview": "",
33
+ "serverTimePretty": "20 août 2021 21:35:18",
34
+ "pageId": "19696671",
35
+ "eventCategory": "Ecommerce",
36
+ "eventAction": "Cart change",
37
+ "timeSpent": 48,
38
+ "timeSpentPretty": "48s",
39
+ "pageviewPosition": "8",
40
+ "timestamp": 1629495318,
41
+ "icon": "plugins/Morpheus/images/event.png",
42
+ "iconSVG": "plugins/Morpheus/images/event.svg",
43
+ "title": "Evènement",
44
+ "subtitle": "Catégorie: \"Ecommerce', Action: \"Cart change\"",
45
+ "eventName": "added - Basic Wetsuit",
46
+ "eventValue": 1,
47
+ "dimension2": "julien",
48
+ "dimension4": "indonesia",
49
+ "dimension5": "diving",
50
+ "customVariables": {
51
+ "1": {
52
+ "customVariableName1": "page-author",
53
+ "customVariableValue1": "Julien"
54
+ },
55
+ "2": {
56
+ "customVariableName2": "post-age",
57
+ "customVariableValue2": "-430 days"
58
+ }
59
+ }
60
+ },
61
+ {
62
+ "type": "action",
63
+ "url": "https://dive-shop.net/products/diving-boots/",
64
+ "pageTitle": "Divezone Brand Diving Boots - Divezone Store",
65
+ "pageIdAction": "60",
66
+ "idpageview": "8CDIez",
67
+ "serverTimePretty": "20 août 2021 21:30:25",
68
+ "pageId": "19696664",
69
+ "timeSpent": "2",
70
+ "timeSpentPretty": "2s",
71
+ "pageviewPosition": "5",
72
+ "title": "Divezone Brand Diving Boots - Divezone Store",
73
+ "subtitle": "https://dive-shop.net/products/diving-boots/",
74
+ "icon": "",
75
+ "iconSVG": "plugins/Morpheus/images/action.svg",
76
+ "timestamp": 1629495025,
77
+ "dimension2": "julien",
78
+ "dimension4": "indonesia",
79
+ "dimension5": "diving"
80
+ },
81
+ {
82
+ "type": "search",
83
+ "url": "https://dive-shop.net/products/diving-boots/",
84
+ "pageTitle": "Divezone Brand Diving Boots - Divezone Store",
85
+ "pageIdAction": "60",
86
+ "idpageview": "8CDIez",
87
+ "serverTimePretty": "20 août 2021 21:30:25",
88
+ "pageId": "19696664",
89
+ "timeSpent": "2",
90
+ "timeSpentPretty": "2s",
91
+ "pageviewPosition": "5",
92
+ "title": "Divezone Brand Diving Boots - Divezone Store",
93
+ "subtitle": "https://dive-shop.net/products/diving-boots/",
94
+ "icon": "",
95
+ "iconSVG": "plugins/Morpheus/images/action.svg",
96
+ "timestamp": 1629495022,
97
+ "dimension2": "julien",
98
+ "dimension4": "indonesia",
99
+ "dimension5": "diving",
100
+ "siteSearchKeyword": "scuba"
101
+ }
102
+ ]
103
+ }
package/dist/config.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RESULTPERPAGE = exports.INITIAL_OFFSET = exports.DESTINATION_TABLE = exports.PGDATABASE = exports.MATOMO_SITE = exports.MATOMO_URL = exports.MATOMO_KEY = void 0;
4
+ exports.MATOMO_KEY = process.env.MATOMO_KEY || "";
5
+ exports.MATOMO_URL = process.env.MATOMO_URL || "https://matomo.fabrique.social.gouv.fr/";
6
+ exports.MATOMO_SITE = process.env.MATOMO_SITE || 0;
7
+ exports.PGDATABASE = process.env.PGDATABASE || "";
8
+ exports.DESTINATION_TABLE = process.env.DESTINATION_TABLE || "matomo";
9
+ exports.INITIAL_OFFSET = process.env.INITIAL_OFFSET || "3";
10
+ exports.RESULTPERPAGE = process.env.RESULTPERPAGE || "500";
package/dist/db.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.db = void 0;
7
+ const pg_1 = require("pg");
8
+ const kysely_1 = require("kysely");
9
+ const debug_1 = __importDefault(require("debug"));
10
+ const config_1 = require("./config");
11
+ const debug = (0, debug_1.default)("db");
12
+ exports.db = new kysely_1.Kysely({
13
+ dialect: new kysely_1.PostgresDialect({
14
+ pool: new pg_1.Pool({
15
+ connectionString: config_1.PGDATABASE,
16
+ }),
17
+ }),
18
+ log(event) {
19
+ if (event.level === "query") {
20
+ // debug(event.query.sql);
21
+ // debug(event.query.parameters);
22
+ }
23
+ },
24
+ });
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.importDate = void 0;
16
+ const p_all_1 = __importDefault(require("p-all"));
17
+ const debug_1 = __importDefault(require("debug"));
18
+ const formatISO_1 = __importDefault(require("date-fns/formatISO"));
19
+ const kysely_1 = require("kysely");
20
+ const db_1 = require("./db");
21
+ const importEvent_1 = require("./importEvent");
22
+ const config_1 = require("./config");
23
+ const debug = (0, debug_1.default)("importDate");
24
+ /** return date as ISO yyyy-mm-dd */
25
+ const isoDate = (date) => (0, formatISO_1.default)(date, { representation: "date" });
26
+ /** check how many visits complete for a given date */
27
+ const getRecordsCount = (date) => __awaiter(void 0, void 0, void 0, function* () {
28
+ const result = yield db_1.db
29
+ .selectFrom(config_1.DESTINATION_TABLE)
30
+ .select(db_1.db.fn.count("idvisit").distinct().as("count"))
31
+ // UTC to be iso with matomo matomo data
32
+ .where((0, kysely_1.sql) `date(timezone('UTC', action_timestamp))`, "=", date)
33
+ .executeTakeFirst();
34
+ // start at previous visit in case action didnt finished to record
35
+ const count = Math.max(0, (result && parseInt(result.count) - 1) || 0);
36
+ return count;
37
+ });
38
+ /** import all event from givent date */
39
+ const importDate = (piwikApi, date, filterOffset = 0) => __awaiter(void 0, void 0, void 0, function* () {
40
+ const limit = parseInt(config_1.RESULTPERPAGE);
41
+ const offset = filterOffset || (yield getRecordsCount(isoDate(date)));
42
+ if (!offset) {
43
+ debug(`${isoDate(date)}: load ${limit} visits`);
44
+ }
45
+ else {
46
+ debug(`${isoDate(date)}: load ${limit} more visits after ${offset}`);
47
+ }
48
+ // fetch visits details
49
+ const visits = yield new Promise((resolve) => piwikApi({
50
+ method: "Live.getLastVisitsDetails",
51
+ period: "day",
52
+ date: isoDate(date),
53
+ // minTimestamp: isoDate(new Date()) === isoDate(date) ? date.getTime() / 1000 : undefined, // if today, dont go further (??)
54
+ filter_limit: limit,
55
+ filter_offset: offset,
56
+ filter_sort_order: "asc",
57
+ idSite: config_1.MATOMO_SITE,
58
+ }, (err, visits = []) => {
59
+ if (err) {
60
+ console.error("err", err);
61
+ resolve([]);
62
+ }
63
+ return resolve(visits);
64
+ }));
65
+ debug(`fetched ${visits.length} visits`);
66
+ // flatten all events
67
+ const eventsFromVisits = visits.flatMap(importEvent_1.getEventsFromMatomoVisit);
68
+ const allEvents = eventsFromVisits.filter((event) => {
69
+ return true;
70
+ });
71
+ if (!allEvents.length) {
72
+ debug(`no more valid events after ${isoDate(date)}`);
73
+ return [];
74
+ }
75
+ debug(`import ${allEvents.length} events`);
76
+ // serial-import events into PG
77
+ const importedEvents = yield (0, p_all_1.default)(allEvents.map((event) => () => (0, importEvent_1.importEvent)(event)), { concurrency: 10, stopOnError: true });
78
+ // continue to next page if necessary
79
+ if (visits.length === limit) {
80
+ const nextOffset = offset + limit;
81
+ const nextEvents = yield (0, exports.importDate)(piwikApi, date, nextOffset);
82
+ return [...importedEvents, ...(nextEvents || [])];
83
+ }
84
+ debug(`finished importing ${isoDate(date)}, offset ${offset}`);
85
+ return importedEvents || [];
86
+ });
87
+ exports.importDate = importDate;
88
+ module.exports = { importDate: exports.importDate };
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getEventsFromMatomoVisit = exports.importEvent = void 0;
4
+ const config_1 = require("./config");
5
+ const db_1 = require("./db");
6
+ /**
7
+ *
8
+ * @param {Client} client
9
+ * @param {import("types").Event} event
10
+ *
11
+ * @return {Promise<Record<"rows", any[]>>}
12
+ */
13
+ const importEvent = (event) =>
14
+ // @ts-ignore // TODO
15
+ db_1.db
16
+ .insertInto(config_1.DESTINATION_TABLE)
17
+ .values([Object.assign({}, event)])
18
+ .onConflict((oc) => oc.doNothing())
19
+ .execute();
20
+ exports.importEvent = importEvent;
21
+ const matomoProps = [
22
+ "idSite",
23
+ "idVisit",
24
+ "actions",
25
+ "country",
26
+ "region",
27
+ "city",
28
+ "operatingSystemName",
29
+ "deviceModel",
30
+ "deviceBrand",
31
+ "visitDuration",
32
+ "daysSinceFirstVisit",
33
+ "visitorType",
34
+ "visitorId",
35
+ "referrerType",
36
+ "referrerName",
37
+ "siteName",
38
+ "userId",
39
+ ];
40
+ /** @type Record<string, (a: import("types/matomo-api").ActionDetail) => string | number> */
41
+ const actionProps = {
42
+ action_type: (action) => action.type,
43
+ action_title: (action) => action.title,
44
+ action_eventcategory: (action) => action.eventCategory,
45
+ action_eventaction: (action) => action.eventAction,
46
+ action_eventname: (action) => action.eventName,
47
+ action_eventvalue: (action) => action.eventValue,
48
+ action_timespent: (action) => action.timeSpent,
49
+ action_timestamp: (action) => new Date(action.timestamp * 1000).toISOString(),
50
+ action_url: (action) => action.url,
51
+ sitesearchkeyword: (action) => action.siteSearchKeyword,
52
+ };
53
+ const getEventsFromMatomoVisit = (matomoVisit) => {
54
+ return matomoVisit.actionDetails.map((actionDetail, actionIndex) => {
55
+ const usercustomproperties = {};
56
+ for (let k = 1; k < 10; k++) {
57
+ const property = actionDetail.customVariables && actionDetail.customVariables[k];
58
+ if (!property)
59
+ continue; // max 10 custom variables
60
+ //@ts-ignore
61
+ usercustomproperties[property[`customVariableName${k}`]] = property[`customVariableValue${k}`];
62
+ }
63
+ /** @type {Record<string, string>} */
64
+ const usercustomdimensions = {};
65
+ for (let k = 1; k < 11; k++) {
66
+ const dimension = `dimension${k}`;
67
+ //@ts-ignore
68
+ const value = actionDetail[dimension] || matomoVisit[dimension];
69
+ if (!value)
70
+ continue; // max 10 custom variables
71
+ //@ts-ignore
72
+ usercustomdimensions[dimension] = value;
73
+ }
74
+ const event = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, matomoProps.reduce((a, prop) => (Object.assign(Object.assign({}, a), { [prop.toLowerCase()]: matomoVisit[prop] })), {})), { serverdateprettyfirstaction: new Date((matomoVisit.firstActionTimestamp || 0) * 1000).toISOString() }), Object.keys(actionProps).reduce((a, prop) => (Object.assign(Object.assign({}, a), { [prop.toLowerCase()]: actionProps[prop](actionDetail) })), {
75
+ action_id: `${matomoVisit.idVisit}_${actionIndex}`,
76
+ })), {
77
+ // custom variables
78
+ usercustomproperties,
79
+ // custom dimensions
80
+ // We keep both for backwards compatibility.
81
+ // Current implementation is flat with one column for each dimension.
82
+ usercustomdimensions }), usercustomdimensions);
83
+ return event;
84
+ });
85
+ };
86
+ exports.getEventsFromMatomoVisit = getEventsFromMatomoVisit;
package/dist/index.js ADDED
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const kysely_1 = require("kysely");
16
+ const p_all_1 = __importDefault(require("p-all"));
17
+ const debug_1 = __importDefault(require("debug"));
18
+ const eachDayOfInterval_1 = __importDefault(require("date-fns/eachDayOfInterval"));
19
+ const piwik_client_1 = __importDefault(require("piwik-client"));
20
+ const db_1 = require("./db");
21
+ const config_1 = require("./config");
22
+ const importDate_1 = require("./importDate");
23
+ const debug = (0, debug_1.default)("index");
24
+ function run(date) {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ debug("run, date=" + date);
27
+ const piwik = new piwik_client_1.default(config_1.MATOMO_URL, config_1.MATOMO_KEY);
28
+ // priority:
29
+ // - optional parameter date
30
+ // - last event in the table
31
+ // - optional env.STARTDATE
32
+ // - today
33
+ let referenceDate;
34
+ if (!referenceDate && date)
35
+ referenceDate = new Date(date);
36
+ if (!referenceDate)
37
+ referenceDate = yield findLastEventInMatomo(db_1.db);
38
+ if (!referenceDate && process.env.STARTDATE)
39
+ referenceDate = new Date(process.env.STARTDATE);
40
+ if (!referenceDate)
41
+ referenceDate = new Date(new Date().getTime() - +config_1.INITIAL_OFFSET * 24 * 60 * 60 * 1000);
42
+ const dates = (0, eachDayOfInterval_1.default)({
43
+ start: referenceDate,
44
+ end: new Date(new Date().getTime()),
45
+ });
46
+ debug(`import starting at : ${dates[0].toISOString()}`);
47
+ // for each date, serial-import data
48
+ const res = yield (0, p_all_1.default)(dates.map((date) => () => (0, importDate_1.importDate)(piwik.api.bind(piwik), date)), { concurrency: 1, stopOnError: true });
49
+ debug("close");
50
+ return res;
51
+ });
52
+ }
53
+ exports.default = run;
54
+ if (require.main === module) {
55
+ (() => __awaiter(void 0, void 0, void 0, function* () {
56
+ if (!config_1.MATOMO_SITE)
57
+ return console.error("Missing env MATOMO_SITE");
58
+ if (!config_1.MATOMO_KEY)
59
+ return console.error("Missing env MATOMO_KEY");
60
+ if (!config_1.PGDATABASE)
61
+ return console.error("Missing env PGDATABASE");
62
+ yield run();
63
+ debug("run finished");
64
+ db_1.db.destroy();
65
+ }))();
66
+ }
67
+ function findLastEventInMatomo(db) {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ const latest = yield db
70
+ .selectFrom(config_1.DESTINATION_TABLE)
71
+ .select((0, kysely_1.sql) `action_timestamp at time zone 'UTC'`.as("action_timestamp"))
72
+ .orderBy("action_timestamp", "desc")
73
+ .limit(1)
74
+ .executeTakeFirst();
75
+ if (latest) {
76
+ // check from the day before just to be sure we haev all events
77
+ const date = new Date(new Date(latest.action_timestamp).getTime() - 2 * 24 * 60 * 60 * 1000);
78
+ return date;
79
+ }
80
+ return null;
81
+ });
82
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ const path = __importStar(require("path"));
36
+ const fs_1 = require("fs");
37
+ const kysely_1 = require("kysely");
38
+ const db_1 = require("./db");
39
+ function migrateDown() {
40
+ return __awaiter(this, void 0, void 0, function* () {
41
+ const migrator = new kysely_1.Migrator({
42
+ db: db_1.db,
43
+ provider: new kysely_1.FileMigrationProvider({
44
+ fs: fs_1.promises,
45
+ path,
46
+ migrationFolder: __dirname + "/migrations",
47
+ }),
48
+ });
49
+ const { error, results } = yield migrator.migrateDown();
50
+ results === null || results === void 0 ? void 0 : results.forEach((it) => {
51
+ if (it.status === "Success") {
52
+ console.log(`down migration "${it.migrationName}" was executed successfully`);
53
+ }
54
+ else if (it.status === "Error") {
55
+ console.error(`failed to execute down migration "${it.migrationName}"`);
56
+ }
57
+ });
58
+ if (error) {
59
+ console.error("failed to down migrate");
60
+ console.error(error);
61
+ process.exit(1);
62
+ }
63
+ yield db_1.db.destroy();
64
+ });
65
+ }
66
+ migrateDown();
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ const path = __importStar(require("path"));
36
+ const fs_1 = require("fs");
37
+ const kysely_1 = require("kysely");
38
+ const db_1 = require("./db");
39
+ const config_1 = require("./config");
40
+ function migrateToLatest() {
41
+ return __awaiter(this, void 0, void 0, function* () {
42
+ const extension = yield db_1.db
43
+ .selectFrom("pg_extension")
44
+ //@ts-ignore
45
+ .select("extname")
46
+ //@ts-ignore
47
+ .where("extname", "=", "pg_partman")
48
+ .executeTakeFirst();
49
+ if (extension) {
50
+ console.error("pg_partman extension detected; Skip migrations");
51
+ return;
52
+ }
53
+ const migrator = new kysely_1.Migrator({
54
+ db: db_1.db,
55
+ provider: new kysely_1.FileMigrationProvider({
56
+ fs: fs_1.promises,
57
+ path,
58
+ migrationFolder: __dirname + "/migrations",
59
+ }),
60
+ // allow to have mutliple migratable instances in a single schema
61
+ migrationTableName: `${config_1.DESTINATION_TABLE}_migration`,
62
+ migrationLockTableName: `${config_1.DESTINATION_TABLE}_migration_lock`,
63
+ });
64
+ const { error, results } = yield migrator.migrateToLatest();
65
+ results === null || results === void 0 ? void 0 : results.forEach((it) => {
66
+ if (it.status === "Success") {
67
+ console.log(`migration "${it.migrationName}" was executed successfully`);
68
+ }
69
+ else if (it.status === "Error") {
70
+ console.error(`failed to execute migration "${it.migrationName}"`);
71
+ }
72
+ });
73
+ if (error) {
74
+ console.error("failed to migrate");
75
+ console.error(error);
76
+ process.exit(1);
77
+ }
78
+ else {
79
+ if (!(results === null || results === void 0 ? void 0 : results.length)) {
80
+ console.log("No migration to run");
81
+ }
82
+ }
83
+ yield db_1.db.destroy();
84
+ });
85
+ }
86
+ migrateToLatest();
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.down = exports.up = void 0;
13
+ const kysely_1 = require("kysely");
14
+ const DESTINATION_TABLE = process.env.DESTINATION_TABLE || "matomo";
15
+ function up(db) {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ yield db.schema
18
+ .createTable(DESTINATION_TABLE)
19
+ .ifNotExists()
20
+ .addColumn("action_id", "text", (col) => col.unique().notNull())
21
+ .addColumn("idsite", "text")
22
+ .addColumn("idvisit", "text")
23
+ .addColumn("actions", "text")
24
+ .addColumn("country", "text")
25
+ .addColumn("region", "text")
26
+ .addColumn("city", "text")
27
+ .addColumn("operatingsystemname", "text")
28
+ .addColumn("devicemodel", "text")
29
+ .addColumn("devicebrand", "text")
30
+ .addColumn("visitduration", "text")
31
+ .addColumn("dayssincefirstvisit", "text")
32
+ .addColumn("visitortype", "text")
33
+ .addColumn("sitename", "text")
34
+ .addColumn("userid", "text")
35
+ .addColumn("serverdateprettyfirstaction", "date")
36
+ .addColumn("action_type", "text")
37
+ .addColumn("action_eventcategory", "text")
38
+ .addColumn("action_eventaction", "text")
39
+ .addColumn("action_eventname", "text")
40
+ .addColumn("action_eventvalue", "numeric")
41
+ .addColumn("action_timespent", "text")
42
+ .addColumn("action_timestamp", "timestamptz", (col) => col.defaultTo((0, kysely_1.sql) `now()`))
43
+ .addColumn("usercustomproperties", "json")
44
+ .addColumn("usercustomdimensions", "json")
45
+ .addColumn("dimension1", "text")
46
+ .addColumn("dimension2", "text")
47
+ .addColumn("dimension3", "text")
48
+ .addColumn("dimension4", "text")
49
+ .addColumn("dimension5", "text")
50
+ .addColumn("dimension6", "text")
51
+ .addColumn("dimension7", "text")
52
+ .addColumn("dimension8", "text")
53
+ .addColumn("dimension9", "text")
54
+ .addColumn("dimension10", "text")
55
+ .addColumn("action_url", "text")
56
+ .addColumn("sitesearchkeyword", "text")
57
+ .addColumn("action_title", "text")
58
+ .addColumn("visitorid", "text")
59
+ .addColumn("referrertype", "text")
60
+ .addColumn("referrername", "text")
61
+ .execute();
62
+ });
63
+ }
64
+ exports.up = up;
65
+ function down(db) {
66
+ return __awaiter(this, void 0, void 0, function* () {
67
+ yield db.schema.dropTable(DESTINATION_TABLE).execute();
68
+ });
69
+ }
70
+ exports.down = down;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.down = exports.up = void 0;
13
+ const kysely_1 = require("kysely");
14
+ const DESTINATION_TABLE = process.env.DESTINATION_TABLE || "matomo";
15
+ const indexes = [
16
+ {
17
+ name: "idx_action_eventaction_matomo",
18
+ columns: ["action_eventaction"],
19
+ },
20
+ {
21
+ name: "idx_action_eventcategory_matomo",
22
+ columns: ["action_eventcategory"],
23
+ },
24
+ {
25
+ name: "idx_action_id",
26
+ columns: ["action_id"],
27
+ },
28
+ {
29
+ name: "idx_action_timestamp_matomo",
30
+ columns: ["action_timestamp"],
31
+ },
32
+ {
33
+ name: "idx_action_type_matomo",
34
+ columns: ["action_type"],
35
+ },
36
+ {
37
+ name: "idx_actionurl",
38
+ columns: ["action_url"],
39
+ },
40
+ {
41
+ name: "idx_dimension1",
42
+ columns: ["dimension1"],
43
+ },
44
+ {
45
+ name: "idx_dimension2",
46
+ columns: ["dimension2"],
47
+ },
48
+ {
49
+ name: "idx_dimension3",
50
+ columns: ["dimension3"],
51
+ },
52
+ {
53
+ name: "idx_dimension4",
54
+ columns: ["dimension4"],
55
+ },
56
+ {
57
+ name: "idx_dimension5",
58
+ columns: ["dimension5"],
59
+ },
60
+ {
61
+ name: "idx_idvisit_matomo",
62
+ columns: ["idvisit"],
63
+ },
64
+ {
65
+ name: "idx_region",
66
+ columns: ["region"],
67
+ },
68
+ {
69
+ name: "idx_userid",
70
+ columns: ["userid"],
71
+ },
72
+ {
73
+ name: "idx_visitorid",
74
+ columns: ["visitorid"],
75
+ },
76
+ ];
77
+ function up(db) {
78
+ return __awaiter(this, void 0, void 0, function* () {
79
+ indexes.forEach((index) => __awaiter(this, void 0, void 0, function* () {
80
+ yield db.schema
81
+ .createIndex(index.name)
82
+ .ifNotExists()
83
+ .on(DESTINATION_TABLE)
84
+ .using("btree")
85
+ .columns(index.columns)
86
+ .execute();
87
+ }));
88
+ yield db.schema
89
+ .createIndex("actions_day")
90
+ .ifNotExists()
91
+ .on(DESTINATION_TABLE)
92
+ .expression((0, kysely_1.sql) `date(timezone('UTC', action_timestamp))`)
93
+ .execute();
94
+ });
95
+ }
96
+ exports.up = up;
97
+ function down(db) {
98
+ return __awaiter(this, void 0, void 0, function* () {
99
+ indexes.forEach((index) => __awaiter(this, void 0, void 0, function* () {
100
+ yield db.schema.dropIndex(index.name).execute();
101
+ }));
102
+ db.schema.dropIndex("actions_day").execute();
103
+ });
104
+ }
105
+ exports.down = down;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@socialgouv/matomo-postgres",
3
3
  "description": "Extract visitor events from Matomo API and push to Postgres",
4
- "version": "1.5.2-beta.1",
4
+ "version": "1.5.2-beta.3",
5
5
  "types": "types/index.d.ts",
6
6
  "license": "Apache-2.0",
7
7
  "main": "dist/index.js",