@socialgouv/matomo-postgres 1.5.0 → 1.5.2-beta.1
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.
- package/README.md +13 -12
- package/bin/index.js +1 -1
- package/package.json +24 -12
- package/.github/workflows/release.yml +0 -33
- package/.github/workflows/test.yml +0 -27
- package/docker/Dockerfile +0 -43
- package/docker/initdb.sh +0 -13
- package/docker-compose.yml +0 -18
- package/header.png +0 -0
- package/initial.sql +0 -73
- package/jsconfig.json +0 -10
- package/renovate.json +0 -6
- package/src/__tests__/__snapshots__/importDate.test.js.snap +0 -389
- package/src/__tests__/__snapshots__/importEvent.test.js.snap +0 -359
- package/src/__tests__/__snapshots__/index.test.js.snap +0 -56
- package/src/__tests__/importDate.test.js +0 -133
- package/src/__tests__/importEvent.test.js +0 -22
- package/src/__tests__/index.test.js +0 -194
- package/src/__tests__/visit.json +0 -103
- package/src/config.js +0 -9
- package/src/createTable.js +0 -83
- package/src/importDate.js +0 -105
- package/src/importEvent.js +0 -108
- package/src/index.js +0 -79
- package/types/index.d.ts +0 -31
- package/types/matomo.d.ts +0 -214
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
const mock_pgQuery = jest.fn();
|
|
2
|
-
const mock_matomoApi = jest.fn();
|
|
3
|
-
const formatISO = require("date-fns/formatISO");
|
|
4
|
-
const addDays = require("date-fns/addDays");
|
|
5
|
-
|
|
6
|
-
const { OFFSET } = require("../config");
|
|
7
|
-
|
|
8
|
-
process.env.MATOMO_SITE = "42";
|
|
9
|
-
process.env.PROJECT_NAME = "some-project";
|
|
10
|
-
|
|
11
|
-
const matomoVisit = require("./visit.json");
|
|
12
|
-
|
|
13
|
-
const run = require("../index");
|
|
14
|
-
|
|
15
|
-
const NB_REQUEST_TO_INIT_DB = 3; // Number of query to init DB (createTable.js)
|
|
16
|
-
const TEST_DATE = new Date();
|
|
17
|
-
|
|
18
|
-
// @ts-ignore
|
|
19
|
-
const isoDate = (date) => formatISO(date, { representation: "date" });
|
|
20
|
-
|
|
21
|
-
jest.mock("pg", () => {
|
|
22
|
-
class Client {
|
|
23
|
-
escapeIdentifier(name) {
|
|
24
|
-
return name;
|
|
25
|
-
}
|
|
26
|
-
end() {}
|
|
27
|
-
connect() {
|
|
28
|
-
return Promise.resolve();
|
|
29
|
-
}
|
|
30
|
-
query(...args) {
|
|
31
|
-
return Promise.resolve(mock_pgQuery(...args));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
Client,
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
jest.mock("piwik-client", () => {
|
|
40
|
-
class MatomoClient {
|
|
41
|
-
api(...args) {
|
|
42
|
-
return mock_matomoApi(...args);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return MatomoClient;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
jest.resetAllMocks();
|
|
50
|
-
jest.resetModules();
|
|
51
|
-
process.env.STARTDATE = "";
|
|
52
|
-
//process.env.OFFSET = "";
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("run: should create table", async () => {
|
|
56
|
-
mock_pgQuery.mockReturnValue({ rows: [] });
|
|
57
|
-
mock_matomoApi.mockImplementation((options, cb) => {
|
|
58
|
-
return cb(null, []);
|
|
59
|
-
});
|
|
60
|
-
await run();
|
|
61
|
-
expect(mock_pgQuery.mock.calls[0]).toMatchSnapshot();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("run: should fetch the latest event date if no date provided", async () => {
|
|
65
|
-
jest.useFakeTimers("modern").setSystemTime(TEST_DATE.getTime());
|
|
66
|
-
mock_pgQuery.mockReturnValue({ rows: [] });
|
|
67
|
-
|
|
68
|
-
mock_matomoApi.mockImplementation((options, cb) => {
|
|
69
|
-
return cb(null, [
|
|
70
|
-
{
|
|
71
|
-
...matomoVisit,
|
|
72
|
-
idVisit: 123,
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
...matomoVisit,
|
|
76
|
-
idVisit: 124,
|
|
77
|
-
},
|
|
78
|
-
]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
await run();
|
|
82
|
-
|
|
83
|
-
// check matomo requests
|
|
84
|
-
expect(mock_matomoApi.mock.calls[0][0].date).toEqual(isoDate(TEST_DATE));
|
|
85
|
-
expect(mock_matomoApi.mock.calls[0][0].filter_offset).toEqual(0);
|
|
86
|
-
|
|
87
|
-
// check db queries
|
|
88
|
-
expect(mock_pgQuery.mock.calls[NB_REQUEST_TO_INIT_DB][0]).toEqual(
|
|
89
|
-
"select action_timestamp from matomo order by action_timestamp desc limit 1"
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("run: should resume using latest event date - offset if no date provided", async () => {
|
|
94
|
-
jest.useFakeTimers("modern").setSystemTime(TEST_DATE.getTime());
|
|
95
|
-
|
|
96
|
-
const LAST_EVENT_DATE_OFFSET = 2;
|
|
97
|
-
// @ts-ignore
|
|
98
|
-
const LAST_EVENT_DATE = addDays(TEST_DATE, -LAST_EVENT_DATE_OFFSET);
|
|
99
|
-
|
|
100
|
-
mock_pgQuery.mockReturnValue({ rows: [{ action_timestamp: LAST_EVENT_DATE.getTime() }] });
|
|
101
|
-
|
|
102
|
-
mock_matomoApi.mockImplementation((options, cb) => {
|
|
103
|
-
return cb(null, [
|
|
104
|
-
{
|
|
105
|
-
...matomoVisit,
|
|
106
|
-
idVisit: 123,
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
...matomoVisit,
|
|
110
|
-
idVisit: 124,
|
|
111
|
-
},
|
|
112
|
-
]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await run();
|
|
116
|
-
|
|
117
|
-
// check matomo requests
|
|
118
|
-
expect(mock_matomoApi.mock.calls[0][0].date).toEqual(
|
|
119
|
-
// @ts-ignore
|
|
120
|
-
isoDate(addDays(LAST_EVENT_DATE, -parseInt(OFFSET)))
|
|
121
|
-
);
|
|
122
|
-
expect(mock_matomoApi.mock.calls[0][0].filter_offset).toEqual(0);
|
|
123
|
-
|
|
124
|
-
const daysCount = LAST_EVENT_DATE_OFFSET + parseInt(OFFSET) + 1;
|
|
125
|
-
expect(mock_matomoApi.mock.calls.length).toEqual(daysCount);
|
|
126
|
-
|
|
127
|
-
// check db queries
|
|
128
|
-
expect(mock_pgQuery.mock.calls.length).toEqual(NB_REQUEST_TO_INIT_DB + 1 + daysCount * 7); // NB_REQUEST_TO_INIT_DB + select queries + days offset
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test("run: should use today date if nothing in DB", async () => {
|
|
132
|
-
jest.useFakeTimers("modern").setSystemTime(TEST_DATE.getTime());
|
|
133
|
-
mock_pgQuery.mockReturnValue({ rows: [] });
|
|
134
|
-
|
|
135
|
-
mock_matomoApi.mockImplementation((options, cb) => {
|
|
136
|
-
return cb(null, [
|
|
137
|
-
{
|
|
138
|
-
...matomoVisit,
|
|
139
|
-
idVisit: 123,
|
|
140
|
-
},
|
|
141
|
-
]);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
await run();
|
|
145
|
-
|
|
146
|
-
// check matomo requests
|
|
147
|
-
expect(mock_matomoApi.mock.calls.length).toEqual(1);
|
|
148
|
-
expect(mock_matomoApi.mock.calls[0][0].date).toEqual(isoDate(TEST_DATE));
|
|
149
|
-
|
|
150
|
-
// check the 4 events inserted
|
|
151
|
-
expect(mock_pgQuery.mock.calls.length).toEqual(NB_REQUEST_TO_INIT_DB + 5); // NB_REQUEST_TO_INIT_DB + check date + latest + 2 inserts
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("run: should use given date if any", async () => {
|
|
155
|
-
jest.useFakeTimers("modern").setSystemTime(TEST_DATE.getTime());
|
|
156
|
-
mock_pgQuery.mockReturnValue({ rows: [] });
|
|
157
|
-
|
|
158
|
-
mock_matomoApi.mockImplementation((options, cb) => {
|
|
159
|
-
return cb(null, [
|
|
160
|
-
{
|
|
161
|
-
...matomoVisit,
|
|
162
|
-
idVisit: 123,
|
|
163
|
-
},
|
|
164
|
-
]);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// @ts-ignore
|
|
168
|
-
await run(isoDate(addDays(TEST_DATE, -10)) + "T00:00:00.000Z");
|
|
169
|
-
|
|
170
|
-
expect(mock_matomoApi.mock.calls.length).toEqual(11);
|
|
171
|
-
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
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("run: should use STARTDATE if any", async () => {
|
|
175
|
-
// @ts-ignore
|
|
176
|
-
process.env.STARTDATE = isoDate(addDays(TEST_DATE, -5)) + "T00:00:00.000Z";
|
|
177
|
-
jest.useFakeTimers("modern").setSystemTime(TEST_DATE.getTime());
|
|
178
|
-
mock_pgQuery.mockReturnValue({ rows: [] });
|
|
179
|
-
|
|
180
|
-
mock_matomoApi.mockImplementation((options, cb) => {
|
|
181
|
-
return cb(null, [
|
|
182
|
-
{
|
|
183
|
-
...matomoVisit,
|
|
184
|
-
idVisit: 123,
|
|
185
|
-
},
|
|
186
|
-
]);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
await run();
|
|
190
|
-
|
|
191
|
-
expect(mock_matomoApi.mock.calls.length).toEqual(6);
|
|
192
|
-
|
|
193
|
-
expect(mock_pgQuery.mock.calls.length).toEqual(NB_REQUEST_TO_INIT_DB + 1 + 6 * 4); // NB_REQUEST_TO_INIT_DB + initial select + inserts.
|
|
194
|
-
});
|
package/src/__tests__/visit.json
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
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/src/config.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
const MATOMO_KEY = process.env.MATOMO_KEY || "";
|
|
2
|
-
const MATOMO_URL = process.env.MATOMO_URL || "https://matomo.fabrique.social.gouv.fr/";
|
|
3
|
-
const MATOMO_SITE = process.env.MATOMO_SITE || 0;
|
|
4
|
-
const PGDATABASE = process.env.PGDATABASE || "";
|
|
5
|
-
const DESTINATION_TABLE = process.env.DESTINATION_TABLE || "matomo";
|
|
6
|
-
const OFFSET = process.env.OFFSET || "3";
|
|
7
|
-
const RESULTPERPAGE = process.env.RESULTPERPAGE || "500";
|
|
8
|
-
|
|
9
|
-
module.exports = { MATOMO_KEY, MATOMO_URL, MATOMO_SITE, PGDATABASE, DESTINATION_TABLE, OFFSET, RESULTPERPAGE };
|
package/src/createTable.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
const { Client } = require("pg");
|
|
2
|
-
|
|
3
|
-
const { DESTINATION_TABLE } = require("./config");
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
* @param {Client} client
|
|
8
|
-
*/
|
|
9
|
-
async function createTable(client) {
|
|
10
|
-
const table = client.escapeIdentifier(DESTINATION_TABLE);
|
|
11
|
-
const text = `
|
|
12
|
-
|
|
13
|
-
CREATE SCHEMA IF NOT EXISTS partman;
|
|
14
|
-
CREATE EXTENSION IF NOT EXISTS pg_partman SCHEMA partman;
|
|
15
|
-
CREATE TABLE IF NOT EXISTS ${table}
|
|
16
|
-
(
|
|
17
|
-
idsite text,
|
|
18
|
-
idvisit text,
|
|
19
|
-
actions text,
|
|
20
|
-
country text,
|
|
21
|
-
region text,
|
|
22
|
-
city text,
|
|
23
|
-
operatingsystemname text,
|
|
24
|
-
devicemodel text,
|
|
25
|
-
devicebrand text,
|
|
26
|
-
visitduration text,
|
|
27
|
-
dayssincefirstvisit text,
|
|
28
|
-
visitortype text,
|
|
29
|
-
sitename text,
|
|
30
|
-
userid text,
|
|
31
|
-
serverdateprettyfirstaction date,
|
|
32
|
-
action_id text,
|
|
33
|
-
action_type text,
|
|
34
|
-
action_eventcategory text,
|
|
35
|
-
action_eventaction text,
|
|
36
|
-
action_eventname text,
|
|
37
|
-
action_eventvalue decimal,
|
|
38
|
-
action_timespent text,
|
|
39
|
-
action_timestamp timestamp with time zone DEFAULT now(),
|
|
40
|
-
usercustomproperties json,
|
|
41
|
-
usercustomdimensions json,
|
|
42
|
-
dimension1 text,
|
|
43
|
-
dimension2 text,
|
|
44
|
-
dimension3 text,
|
|
45
|
-
dimension4 text,
|
|
46
|
-
dimension5 text,
|
|
47
|
-
dimension6 text,
|
|
48
|
-
dimension7 text,
|
|
49
|
-
dimension8 text,
|
|
50
|
-
dimension9 text,
|
|
51
|
-
dimension10 text,
|
|
52
|
-
action_url text,
|
|
53
|
-
sitesearchkeyword text,
|
|
54
|
-
action_title text,
|
|
55
|
-
visitorid text,
|
|
56
|
-
referrertype text,
|
|
57
|
-
referrername text
|
|
58
|
-
) PARTITION BY RANGE (action_timestamp);
|
|
59
|
-
`;
|
|
60
|
-
|
|
61
|
-
await client.query(text, []);
|
|
62
|
-
|
|
63
|
-
const migrations = [
|
|
64
|
-
`CREATE INDEX IF NOT EXISTS idx_dimension1 ON matomo(dimension1);
|
|
65
|
-
CREATE INDEX IF NOT EXISTS idx_dimension2 ON matomo(dimension2);
|
|
66
|
-
CREATE INDEX IF NOT EXISTS idx_dimension3 ON matomo(dimension3);
|
|
67
|
-
CREATE INDEX IF NOT EXISTS idx_dimension4 ON matomo(dimension4);
|
|
68
|
-
CREATE INDEX IF NOT EXISTS idx_dimension5 ON matomo(dimension5);
|
|
69
|
-
CREATE INDEX IF NOT EXISTS idx_userid ON matomo(userid);
|
|
70
|
-
CREATE INDEX IF NOT EXISTS idx_actionurl ON matomo(action_url);
|
|
71
|
-
CREATE INDEX IF NOT EXISTS idx_region ON matomo(region);`,
|
|
72
|
-
`ALTER TABLE matomo ADD COLUMN IF NOT EXISTS visitorid text;
|
|
73
|
-
ALTER TABLE matomo ADD COLUMN IF NOT EXISTS referrertype text;
|
|
74
|
-
ALTER TABLE matomo ADD COLUMN IF NOT EXISTS referrername text;
|
|
75
|
-
CREATE INDEX IF NOT EXISTS idx_visitorid ON matomo(visitorid);`,
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
for (const query of migrations) {
|
|
79
|
-
await client.query(query, []);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
module.exports = { createTable };
|
package/src/importDate.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
const { Client } = require("pg");
|
|
2
|
-
const pAll = require("p-all");
|
|
3
|
-
const debug = require("debug")("importDate");
|
|
4
|
-
const formatISO = require("date-fns/formatISO");
|
|
5
|
-
|
|
6
|
-
const { importEvent, getEventsFromMatomoVisit } = require("./importEvent");
|
|
7
|
-
|
|
8
|
-
const { RESULTPERPAGE, MATOMO_SITE, DESTINATION_TABLE } = require("./config");
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* return date as ISO yyyy-mm-dd
|
|
12
|
-
*
|
|
13
|
-
* @param {Date} date date
|
|
14
|
-
*
|
|
15
|
-
* @returns {string}
|
|
16
|
-
*/
|
|
17
|
-
// @ts-ignore
|
|
18
|
-
const isoDate = (date) => formatISO(date, { representation: "date" });
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* check count if imported rows for given date
|
|
22
|
-
*
|
|
23
|
-
* @param {Client} client PG client
|
|
24
|
-
* @param {Date} date datetime as ISO string
|
|
25
|
-
*
|
|
26
|
-
* @returns {Promise<any>}
|
|
27
|
-
*/
|
|
28
|
-
const getRecordsCount = async (client, date) => {
|
|
29
|
-
const text = `SELECT COUNT(distinct idvisit) FROM ${client.escapeIdentifier(
|
|
30
|
-
DESTINATION_TABLE
|
|
31
|
-
)} WHERE action_timestamp::date='${isoDate(date)}';`;
|
|
32
|
-
const result = await client.query(text);
|
|
33
|
-
return parseInt((result && result.rows && result.rows.length && result.rows[0].count) || 0);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* import all matomo data for a given date
|
|
38
|
-
*
|
|
39
|
-
* @param {Client} client PG client
|
|
40
|
-
* @param {any} piwikApi piwik.api instance
|
|
41
|
-
* @param {Date} date datetime as ISO string
|
|
42
|
-
*
|
|
43
|
-
* @returns {Promise<any[]>}
|
|
44
|
-
*/
|
|
45
|
-
const importDate = async (client, piwikApi, date, filterOffset = 0) => {
|
|
46
|
-
const limit = parseInt(RESULTPERPAGE);
|
|
47
|
-
const offset = filterOffset || (await getRecordsCount(client, date));
|
|
48
|
-
if (!offset) {
|
|
49
|
-
debug(`${isoDate(date)}: load ${limit} visits`);
|
|
50
|
-
} else {
|
|
51
|
-
debug(`${isoDate(date)}: load ${limit} more visits after ${offset}`);
|
|
52
|
-
}
|
|
53
|
-
// fetch visits details
|
|
54
|
-
const visits = await new Promise((resolve) =>
|
|
55
|
-
piwikApi(
|
|
56
|
-
{
|
|
57
|
-
method: "Live.getLastVisitsDetails",
|
|
58
|
-
period: "day",
|
|
59
|
-
date: isoDate(date),
|
|
60
|
-
filter_limit: limit,
|
|
61
|
-
filter_offset: offset,
|
|
62
|
-
filter_sort_order: "asc",
|
|
63
|
-
idSite: MATOMO_SITE,
|
|
64
|
-
},
|
|
65
|
-
(err, responseObject = []) => {
|
|
66
|
-
if (err) {
|
|
67
|
-
console.error("err", err);
|
|
68
|
-
resolve([]);
|
|
69
|
-
}
|
|
70
|
-
return resolve(responseObject || []);
|
|
71
|
-
}
|
|
72
|
-
)
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
debug(`fetched ${visits.length} visits`);
|
|
76
|
-
|
|
77
|
-
// flatten events
|
|
78
|
-
const allEvents = visits.flatMap(getEventsFromMatomoVisit);
|
|
79
|
-
|
|
80
|
-
if (!allEvents.length) {
|
|
81
|
-
debug(`no more valid events after ${isoDate(date)}`);
|
|
82
|
-
return [];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
debug(`import ${allEvents.length} events`);
|
|
86
|
-
|
|
87
|
-
// serial-import events into PG
|
|
88
|
-
const importedEvents = await pAll(
|
|
89
|
-
allEvents.map((event) => () => importEvent(client, event)),
|
|
90
|
-
{ concurrency: 10, stopOnError: true }
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// continue to next page if necessary
|
|
94
|
-
if (visits.length === limit) {
|
|
95
|
-
const nextOffset = offset + limit;
|
|
96
|
-
const nextEvents = await importDate(client, piwikApi, date, nextOffset);
|
|
97
|
-
return [...importedEvents, ...(nextEvents || [])];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
debug(`finished importing ${isoDate(date)}, offset ${offset}`);
|
|
101
|
-
|
|
102
|
-
return importedEvents || [];
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
module.exports = { importDate };
|
package/src/importEvent.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
const { Client } = require("pg");
|
|
2
|
-
|
|
3
|
-
const { DESTINATION_TABLE } = require("./config");
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
* @param {Client} client
|
|
8
|
-
* @param {import("types").Event} event
|
|
9
|
-
*
|
|
10
|
-
* @return {Promise<Record<"rows", any[]>>}
|
|
11
|
-
*/
|
|
12
|
-
const importEvent = (client, event) => {
|
|
13
|
-
const eventKeys = Object.keys(event);
|
|
14
|
-
const text = `insert into ${client.escapeIdentifier(DESTINATION_TABLE)}
|
|
15
|
-
(${eventKeys.join(", ")})
|
|
16
|
-
values (${eventKeys.map((k, i) => `\$${i + 1}`).join(", ")})
|
|
17
|
-
ON CONFLICT DO NOTHING`;
|
|
18
|
-
const values = [...eventKeys.map((k) => event[k])];
|
|
19
|
-
return client.query(text, values).catch((e) => {
|
|
20
|
-
console.log("QUERY error", e);
|
|
21
|
-
return { rows: [] };
|
|
22
|
-
});
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const matomoProps = [
|
|
26
|
-
"idSite",
|
|
27
|
-
"idVisit",
|
|
28
|
-
"actions",
|
|
29
|
-
"country",
|
|
30
|
-
"region",
|
|
31
|
-
"city",
|
|
32
|
-
"operatingSystemName",
|
|
33
|
-
"deviceModel",
|
|
34
|
-
"deviceBrand",
|
|
35
|
-
"visitDuration",
|
|
36
|
-
"daysSinceFirstVisit",
|
|
37
|
-
"visitorType",
|
|
38
|
-
"visitorId",
|
|
39
|
-
"referrerType",
|
|
40
|
-
"referrerName",
|
|
41
|
-
"siteName",
|
|
42
|
-
"userId",
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
/** @type Record<string, (a: import("types/matomo").ActionDetail) => string | number> */
|
|
46
|
-
const actionProps = {
|
|
47
|
-
action_type: (action) => action.type,
|
|
48
|
-
action_title: (action) => action.title,
|
|
49
|
-
action_eventcategory: (action) => action.eventCategory,
|
|
50
|
-
action_eventaction: (action) => action.eventAction,
|
|
51
|
-
action_eventname: (action) => action.eventName,
|
|
52
|
-
action_eventvalue: (action) => action.eventValue,
|
|
53
|
-
action_timespent: (action) => action.timeSpent,
|
|
54
|
-
action_timestamp: (action) => new Date(action.timestamp * 1000).toISOString(),
|
|
55
|
-
action_url: (action) => action.url,
|
|
56
|
-
siteSearchKeyword: (action) => action.siteSearchKeyword,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Convert a single matomo visit {<import("types/matomo").Visit} to multiple {Event[]}
|
|
61
|
-
*
|
|
62
|
-
* @param {Partial<import("types/matomo").Visit>} matomoVisit
|
|
63
|
-
*
|
|
64
|
-
* @return {import("types").Event[]} list of events
|
|
65
|
-
*
|
|
66
|
-
*/
|
|
67
|
-
const getEventsFromMatomoVisit = (matomoVisit) => {
|
|
68
|
-
return matomoVisit.actionDetails.map((actionDetail, actionIndex) => {
|
|
69
|
-
/** @type {Record<string, string>} */
|
|
70
|
-
const usercustomproperties = {};
|
|
71
|
-
for (let k = 1; k < 10; k++) {
|
|
72
|
-
const property = actionDetail.customVariables && actionDetail.customVariables[k];
|
|
73
|
-
if (!property) continue; // max 10 custom variables
|
|
74
|
-
usercustomproperties[property[`customVariableName${k}`]] = property[`customVariableValue${k}`];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** @type {Record<string, string>} */
|
|
78
|
-
const usercustomdimensions = {};
|
|
79
|
-
for (let k = 1; k < 11; k++) {
|
|
80
|
-
const dimension = `dimension${k}`;
|
|
81
|
-
const value = actionDetail[dimension] || matomoVisit[dimension];
|
|
82
|
-
if (!value) continue; // max 10 custom variables
|
|
83
|
-
usercustomdimensions[dimension] = value;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** @type {import("types").Event} */
|
|
87
|
-
// @ts-ignore
|
|
88
|
-
const event = {
|
|
89
|
-
// default matomo visit properties
|
|
90
|
-
...matomoProps.reduce((a, prop) => ({ ...a, [prop.toLowerCase()]: matomoVisit[prop] }), {}),
|
|
91
|
-
serverdateprettyfirstaction: new Date((matomoVisit.firstActionTimestamp || 0) * 1000).toISOString(),
|
|
92
|
-
// action specific properties
|
|
93
|
-
...Object.keys(actionProps).reduce((a, prop) => ({ ...a, [prop]: actionProps[prop](actionDetail) }), {
|
|
94
|
-
action_id: `${matomoVisit.idVisit}_${actionIndex}`,
|
|
95
|
-
}),
|
|
96
|
-
// custom variables
|
|
97
|
-
usercustomproperties,
|
|
98
|
-
// custom dimensions
|
|
99
|
-
// We keep both for backwards compatibility.
|
|
100
|
-
// Current implementation is flat with one column for each dimension.
|
|
101
|
-
usercustomdimensions,
|
|
102
|
-
...usercustomdimensions,
|
|
103
|
-
};
|
|
104
|
-
return event;
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
module.exports = { getEventsFromMatomoVisit, importEvent };
|
package/src/index.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
const pAll = require("p-all");
|
|
2
|
-
const debug = require("debug")("index");
|
|
3
|
-
const eachDayOfInterval = require("date-fns/eachDayOfInterval");
|
|
4
|
-
const PiwikClient = require("piwik-client");
|
|
5
|
-
const { Client } = require("pg");
|
|
6
|
-
|
|
7
|
-
const { MATOMO_KEY, MATOMO_URL, MATOMO_SITE, PGDATABASE, DESTINATION_TABLE, OFFSET } = require("./config");
|
|
8
|
-
|
|
9
|
-
const { createTable } = require("./createTable");
|
|
10
|
-
const { importDate } = require("./importDate");
|
|
11
|
-
|
|
12
|
-
// run a sync with a 3-days range
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
* @param {string} [date]
|
|
16
|
-
* @returns
|
|
17
|
-
*/
|
|
18
|
-
async function run(date) {
|
|
19
|
-
debug("run, date=" + date);
|
|
20
|
-
const client = new Client({ connectionString: PGDATABASE });
|
|
21
|
-
await client.connect();
|
|
22
|
-
|
|
23
|
-
const piwik = new PiwikClient(MATOMO_URL, MATOMO_KEY);
|
|
24
|
-
|
|
25
|
-
await createTable(client);
|
|
26
|
-
|
|
27
|
-
// priority:
|
|
28
|
-
// - optional parameter date
|
|
29
|
-
// - last event in the table
|
|
30
|
-
// - optional env.STARTDATE
|
|
31
|
-
// - today
|
|
32
|
-
|
|
33
|
-
let referenceDate;
|
|
34
|
-
if (!referenceDate && date) referenceDate = new Date(date);
|
|
35
|
-
if (!referenceDate) referenceDate = await findLastEventInMatomo(client);
|
|
36
|
-
if (!referenceDate && process.env.STARTDATE) referenceDate = new Date(process.env.STARTDATE);
|
|
37
|
-
if (!referenceDate) referenceDate = new Date();
|
|
38
|
-
|
|
39
|
-
// @ts-ignore
|
|
40
|
-
const dates = eachDayOfInterval({
|
|
41
|
-
start: referenceDate,
|
|
42
|
-
end: new Date(),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
debug(`import : ${dates.join(", ")}`);
|
|
46
|
-
|
|
47
|
-
// for each date, serial-import data
|
|
48
|
-
const res = await pAll(
|
|
49
|
-
dates.map((date) => () => importDate(client, piwik.api.bind(piwik), date)),
|
|
50
|
-
{ concurrency: 1, stopOnError: true }
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
await client.end();
|
|
54
|
-
debug("close");
|
|
55
|
-
|
|
56
|
-
return res;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
module.exports = run;
|
|
60
|
-
|
|
61
|
-
if (require.main === module) {
|
|
62
|
-
(async () => {
|
|
63
|
-
if (!MATOMO_SITE) return console.error("Missing env MATOMO_SITE");
|
|
64
|
-
if (!MATOMO_KEY) return console.error("Missing env MATOMO_KEY");
|
|
65
|
-
if (!PGDATABASE) return console.error("Missing env PGDATABASE");
|
|
66
|
-
await run();
|
|
67
|
-
debug("run finished");
|
|
68
|
-
})();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function findLastEventInMatomo(client) {
|
|
72
|
-
const a = await client.query(
|
|
73
|
-
`select action_timestamp from ${client.escapeIdentifier(DESTINATION_TABLE)} order by action_timestamp desc limit 1`
|
|
74
|
-
);
|
|
75
|
-
if (!a.rows.length || !a.rows[0].action_timestamp) return null;
|
|
76
|
-
const d = new Date(a.rows[0].action_timestamp);
|
|
77
|
-
d.setDate(d.getDate() - +OFFSET);
|
|
78
|
-
return d;
|
|
79
|
-
}
|