@socialgouv/matomo-postgres 2.2.0 → 2.2.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 +199 -32
- package/bin/index.js +10 -12
- package/dist/PiwikClient.js +5 -31
- package/dist/__tests__/importDate.test.js +49 -44
- package/dist/__tests__/importEvent.test.js +3 -8
- package/dist/__tests__/run.test.js +22 -19
- package/dist/config.js +11 -11
- package/dist/db.js +10 -15
- package/dist/importDate.js +19 -27
- package/dist/importEvent.js +127 -59
- package/dist/index.js +25 -44
- package/dist/migrate-down.js +10 -35
- package/dist/migrate-latest.js +46 -62
- package/dist/migrations/20230301-01-initial.js +4 -9
- package/dist/migrations/20230301-02-indexes.js +4 -9
- package/dist/migrations/20250425-01-add-resolution.js +18 -11
- package/dist/migrations/20250715-01-weekly-partitioning.js +28 -31
- package/dist/migrations/20250908-01-convention-analysis-index.js +29 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -2,45 +2,212 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
Extract
|
|
5
|
+
A robust Node.js/TypeScript ETL (Extract, Transform, Load) tool that synchronizes visitor analytics data from Matomo (formerly Piwik) into a PostgreSQL database. Designed for organizations that need to centralize their web analytics data for advanced analysis, reporting, or integration with other systems.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## ✨ Features
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **🔄 Incremental Synchronization** - Smart date range detection with automatic resume capability
|
|
10
|
+
- **📊 Complete Data Extraction** - Captures visitor sessions, events, custom dimensions, and device information
|
|
11
|
+
- **🗄️ Automatic Schema Management** - Kysely-based migrations with performance optimizations
|
|
12
|
+
- **⚡ High Performance** - Controlled concurrency, pagination, and weekly table partitioning
|
|
13
|
+
- **🛡️ Type Safety** - Full TypeScript implementation with comprehensive type definitions
|
|
14
|
+
- **🔍 Detailed Logging** - Progress tracking and debug information for monitoring
|
|
15
|
+
- **📱 Device Analytics** - Screen resolution, device model, and operating system data
|
|
16
|
+
- **🌍 Geographic Data** - Country, region, and city information from visitor sessions
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
## 🚀 Quick Start
|
|
19
|
+
|
|
20
|
+
### Global Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
12
23
|
npx @socialgouv/matomo-postgres
|
|
13
24
|
```
|
|
14
25
|
|
|
15
|
-
###
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
| MATOMO_URL\* | matomo url |
|
|
22
|
-
| PGDATABASE\* | Postgres connection string |
|
|
23
|
-
| DESTINATION_TABLE | `matomo` |
|
|
24
|
-
| STARTDATE | default to today() |
|
|
25
|
-
| RESULTPERPAGE | matomo pagination (defaults to 500) |
|
|
26
|
-
| INITIAL_OFFSET | How many days to fetch on initialisation (defaults to 3) |
|
|
27
|
-
|
|
28
|
-
## Dev
|
|
29
|
-
|
|
30
|
-
```sh
|
|
31
|
-
docker-compose up
|
|
32
|
-
export MATOMO_URL=
|
|
33
|
-
export MATOMO_SITE=
|
|
34
|
-
export MATOMO_KEY=
|
|
35
|
-
export DESTINATION_TABLE= # optional
|
|
36
|
-
export STARTDATE= # optional
|
|
37
|
-
export OFFSET= # optional
|
|
38
|
-
export PGDATABASE=postgres://postgres:postgres@127.0.0.1:5455/postgres
|
|
39
|
-
yarn start
|
|
26
|
+
### Local Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @socialgouv/matomo-postgres
|
|
30
|
+
# or
|
|
31
|
+
yarn add @socialgouv/matomo-postgres
|
|
40
32
|
```
|
|
41
33
|
|
|
42
|
-
|
|
34
|
+
## ⚙️ Configuration
|
|
35
|
+
|
|
36
|
+
### Required Environment Variables
|
|
37
|
+
|
|
38
|
+
| Variable | Description | Example |
|
|
39
|
+
| ------------- | ------------------------------------ | ------------------------------------- |
|
|
40
|
+
| `MATOMO_KEY` | Matomo API authentication token | `your_api_token_here` |
|
|
41
|
+
| `MATOMO_SITE` | Numeric site ID in Matomo | `1` |
|
|
42
|
+
| `MATOMO_URL` | Base URL of your Matomo installation | `https://analytics.example.com/` |
|
|
43
|
+
| `PGDATABASE` | PostgreSQL connection string | `postgresql://user:pass@host:5432/db` |
|
|
44
|
+
|
|
45
|
+
### Optional Environment Variables
|
|
46
|
+
|
|
47
|
+
| Variable | Default | Description |
|
|
48
|
+
| ------------------------------- | -------------------- | ------------------------------------------------------- |
|
|
49
|
+
| `DESTINATION_TABLE` | `matomo` | Selects which table to write to (normal or partitioned) |
|
|
50
|
+
| `MATOMO_TABLE_NAME` | `matomo` | Name for the standard table |
|
|
51
|
+
| `PARTITIONED_MATOMO_TABLE_NAME` | `matomo_partitioned` | Name for the partitioned table |
|
|
52
|
+
| `STARTDATE` | Auto-detected | Override start date for initial import (YYYY-MM-DD) |
|
|
53
|
+
| `RESULTPERPAGE` | `500` | API pagination size (max results per request) |
|
|
54
|
+
| `INITIAL_OFFSET` | `3` | Days to look back on first run |
|
|
55
|
+
|
|
56
|
+
## 🗂️ Table Architecture
|
|
57
|
+
|
|
58
|
+
The tool implements a dual table system to optimize performance for different use cases:
|
|
59
|
+
|
|
60
|
+
### Standard vs Partitioned Tables
|
|
61
|
+
|
|
62
|
+
The application creates both a **standard table** and a **partitioned table**:
|
|
63
|
+
|
|
64
|
+
- **Standard Table** (`MATOMO_TABLE_NAME`): Traditional PostgreSQL table, suitable for smaller datasets or simpler deployments
|
|
65
|
+
- **Partitioned Table** (`PARTITIONED_MATOMO_TABLE_NAME`): Weekly partitioned table optimized for large datasets and improved query performance
|
|
66
|
+
|
|
67
|
+
### Table Selection
|
|
68
|
+
|
|
69
|
+
Use the `DESTINATION_TABLE` environment variable to specify which table receives the imported data:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Write to standard table
|
|
73
|
+
export DESTINATION_TABLE=matomo
|
|
74
|
+
|
|
75
|
+
# Write to partitioned table
|
|
76
|
+
export DESTINATION_TABLE=matomo_partitioned
|
|
77
|
+
|
|
78
|
+
# Write to custom table name
|
|
79
|
+
export DESTINATION_TABLE=my_custom_analytics_table
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### When to Use Partitioned Tables
|
|
83
|
+
|
|
84
|
+
Consider using partitioned tables when:
|
|
85
|
+
|
|
86
|
+
- **Large Data Volumes**: Importing months or years of analytics data
|
|
87
|
+
- **Query Performance**: Need faster queries on specific date ranges
|
|
88
|
+
- **Maintenance Operations**: Easier to manage large datasets with partition pruning
|
|
89
|
+
- **Storage Optimization**: Better compression and maintenance of historical data
|
|
90
|
+
|
|
91
|
+
Both tables share the same schema structure, ensuring compatibility regardless of your choice.
|
|
92
|
+
|
|
93
|
+
## 🏗️ Architecture
|
|
94
|
+
|
|
95
|
+
The tool follows a systematic ETL process:
|
|
96
|
+
|
|
97
|
+
1. **📅 Date Range Detection** - Determines import range based on last sync or configuration
|
|
98
|
+
2. **📥 Data Extraction** - Fetches visitor data from Matomo's `Live.getLastVisitsDetails` API
|
|
99
|
+
3. **🔄 Data Transformation** - Converts visits into structured events with proper typing
|
|
100
|
+
4. **💾 Data Loading** - Inserts events into PostgreSQL with conflict resolution
|
|
101
|
+
5. **📈 Progress Tracking** - Provides detailed logging and resumable operations
|
|
102
|
+
|
|
103
|
+
### Database Schema
|
|
104
|
+
|
|
105
|
+
The tool creates a comprehensive table structure capturing:
|
|
106
|
+
|
|
107
|
+
- **Visitor Information**: IDs, geographic location, device details
|
|
108
|
+
- **Session Metrics**: Duration, visit count, visitor type
|
|
109
|
+
- **Event Data**: Actions, categories, values, timestamps (UTC)
|
|
110
|
+
- **Custom Dimensions**: Flexible JSON fields for custom tracking
|
|
111
|
+
- **Performance Data**: Screen resolution, time spent per action
|
|
112
|
+
|
|
113
|
+
## 🛠️ Development
|
|
114
|
+
|
|
115
|
+
### Local Setup
|
|
116
|
+
|
|
117
|
+
1. **Start PostgreSQL**:
|
|
43
118
|
|
|
44
|
-
|
|
119
|
+
```bash
|
|
120
|
+
docker-compose up
|
|
121
|
+
```
|
|
45
122
|
|
|
46
|
-
|
|
123
|
+
2. **Set Environment Variables**:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
export MATOMO_URL=https://your-matomo-instance/
|
|
127
|
+
export MATOMO_SITE=your_site_id
|
|
128
|
+
export MATOMO_KEY=your_api_token
|
|
129
|
+
export PGDATABASE=postgres://postgres:postgres@127.0.0.1:5455/postgres
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
3. **Run the Application**:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
yarn start
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Development Commands
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Build TypeScript
|
|
142
|
+
yarn build
|
|
143
|
+
|
|
144
|
+
# Run tests
|
|
145
|
+
yarn test
|
|
146
|
+
|
|
147
|
+
# Update test snapshots
|
|
148
|
+
yarn test -u
|
|
149
|
+
|
|
150
|
+
# Lint code
|
|
151
|
+
yarn lint
|
|
152
|
+
|
|
153
|
+
# Fix linting issues
|
|
154
|
+
yarn lint:fix
|
|
155
|
+
|
|
156
|
+
# Run database migrations
|
|
157
|
+
yarn migrate
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 🗄️ Database Migrations
|
|
161
|
+
|
|
162
|
+
Database schema is managed through Kysely migrations located in `./src/migrations/`:
|
|
163
|
+
|
|
164
|
+
Migrations run automatically on each `yarn start` to ensure schema compatibility.
|
|
165
|
+
|
|
166
|
+
## 📊 Data Flow
|
|
167
|
+
|
|
168
|
+
1. **Initialization** - Determine import date range based on:
|
|
169
|
+
- Explicit date parameter
|
|
170
|
+
- Last event timestamp in database
|
|
171
|
+
- `STARTDATE` environment variable
|
|
172
|
+
- Default offset from current date
|
|
173
|
+
|
|
174
|
+
2. **Sequential Processing** - For each date:
|
|
175
|
+
- Check existing records for pagination offset
|
|
176
|
+
- Fetch visitor data in paginated chunks
|
|
177
|
+
- Transform visits into individual events
|
|
178
|
+
- Insert with conflict resolution
|
|
179
|
+
|
|
180
|
+
3. **Concurrency Control**:
|
|
181
|
+
- Sequential date processing (one day at a time)
|
|
182
|
+
- Parallel event insertion (configurable)
|
|
183
|
+
- Automatic pagination for large datasets
|
|
184
|
+
|
|
185
|
+
## 🐛 Troubleshooting
|
|
186
|
+
|
|
187
|
+
### Common Issues
|
|
188
|
+
|
|
189
|
+
**API Authentication Errors**
|
|
190
|
+
|
|
191
|
+
- Verify `MATOMO_KEY` has sufficient permissions
|
|
192
|
+
- Ensure `MATOMO_SITE` ID is correct
|
|
193
|
+
- Check `MATOMO_URL` includes trailing slash
|
|
194
|
+
|
|
195
|
+
**Database Connection Issues**
|
|
196
|
+
|
|
197
|
+
- Verify PostgreSQL is running and accessible
|
|
198
|
+
- Check `PGDATABASE` connection string format
|
|
199
|
+
- Ensure database exists and user has write permissions
|
|
200
|
+
|
|
201
|
+
**Performance Issues**
|
|
202
|
+
|
|
203
|
+
- Adjust `RESULTPERPAGE` for optimal API performance
|
|
204
|
+
- Monitor database indexes and partitioning
|
|
205
|
+
- Consider running during off-peak hours for large imports
|
|
206
|
+
|
|
207
|
+
### Debug Mode
|
|
208
|
+
|
|
209
|
+
Enable detailed logging:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
DEBUG=matomo-postgres* npx @socialgouv/matomo-postgres
|
|
213
|
+
```
|
package/bin/index.js
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { db } from '../dist/db'
|
|
4
|
-
import run from '../dist/index'
|
|
5
|
-
import
|
|
3
|
+
import { db } from '../dist/db.js'
|
|
4
|
+
import run from '../dist/index.js'
|
|
5
|
+
import { startMigration } from '../dist/migrate-latest.js'
|
|
6
6
|
|
|
7
7
|
async function start(date) {
|
|
8
8
|
console.log(`\nRunning migrations\n`)
|
|
9
|
-
await
|
|
9
|
+
await startMigration()
|
|
10
10
|
console.log(`\nStarting import\n`)
|
|
11
11
|
await run(date)
|
|
12
12
|
db.destroy()
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
start(date)
|
|
22
|
-
}
|
|
15
|
+
const date =
|
|
16
|
+
(process.argv[process.argv.length - 1].match(/^\d\d\d\d-\d\d-\d\d$/) &&
|
|
17
|
+
process.argv[process.argv.length - 1]) ||
|
|
18
|
+
''
|
|
19
|
+
console.log(`\nRunning @socialgouv/matomo-postgres ${date}\n`)
|
|
20
|
+
start(date)
|
package/dist/PiwikClient.js
CHANGED
|
@@ -1,33 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
const http = __importStar(require("http"));
|
|
27
|
-
const https = __importStar(require("https"));
|
|
28
|
-
const querystring = __importStar(require("querystring"));
|
|
29
|
-
const url = __importStar(require("url"));
|
|
30
|
-
class PiwikClient {
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as https from 'https';
|
|
3
|
+
import * as querystring from 'querystring';
|
|
4
|
+
import * as url from 'url';
|
|
5
|
+
export default class PiwikClient {
|
|
31
6
|
constructor(baseURL, token) {
|
|
32
7
|
const parsedUrl = url.parse(baseURL, true);
|
|
33
8
|
this.settings = {
|
|
@@ -118,4 +93,3 @@ class PiwikClient {
|
|
|
118
93
|
return req;
|
|
119
94
|
}
|
|
120
95
|
}
|
|
121
|
-
exports.default = PiwikClient;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
2
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
3
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -8,18 +7,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
8
|
});
|
|
10
9
|
};
|
|
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
10
|
// Set environment variables BEFORE any imports
|
|
16
11
|
process.env.MATOMO_SITE = '42';
|
|
17
12
|
process.env.PROJECT_NAME = 'some-project';
|
|
18
13
|
process.env.RESULTPERPAGE = '10';
|
|
19
14
|
process.env.DESTINATION_TABLE = 'matomo';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
import { Pool } from 'pg';
|
|
16
|
+
import { importDate } from '../importDate';
|
|
17
|
+
import matomoVisit from './visit.json';
|
|
23
18
|
const TEST_DATE = new Date(2023, 3, 15);
|
|
24
19
|
let queries = [];
|
|
25
20
|
const result = {
|
|
@@ -43,7 +38,7 @@ jest.mock('pg', () => {
|
|
|
43
38
|
});
|
|
44
39
|
let pool;
|
|
45
40
|
beforeEach(() => {
|
|
46
|
-
pool = new
|
|
41
|
+
pool = new Pool();
|
|
47
42
|
queries = [];
|
|
48
43
|
});
|
|
49
44
|
afterEach(() => {
|
|
@@ -53,12 +48,12 @@ test('importDate: should import given date', () => __awaiter(void 0, void 0, voi
|
|
|
53
48
|
const piwikApi = jest.fn();
|
|
54
49
|
piwikApi.mockImplementation((options, cb) => {
|
|
55
50
|
cb(null, [
|
|
56
|
-
Object.assign(Object.assign({},
|
|
57
|
-
Object.assign(Object.assign({},
|
|
51
|
+
Object.assign(Object.assign({}, matomoVisit), { idVisit: 123 }),
|
|
52
|
+
Object.assign(Object.assign({}, matomoVisit), { idVisit: 124 })
|
|
58
53
|
]);
|
|
59
54
|
});
|
|
60
55
|
pool.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
|
61
|
-
yield
|
|
56
|
+
yield importDate(piwikApi, TEST_DATE);
|
|
62
57
|
expect(piwikApi.mock.calls.length).toEqual(1);
|
|
63
58
|
expect(piwikApi.mock.calls[0][0]).toMatchInlineSnapshot(`
|
|
64
59
|
{
|
|
@@ -79,40 +74,50 @@ test('importDate: should import given date', () => __awaiter(void 0, void 0, voi
|
|
|
79
74
|
],
|
|
80
75
|
]
|
|
81
76
|
`);
|
|
82
|
-
expect(queries.length).toEqual(1 +
|
|
77
|
+
expect(queries.length).toEqual(1 + matomoVisit.actionDetails.length * 2);
|
|
83
78
|
}));
|
|
84
|
-
test('importDate: should
|
|
79
|
+
test('importDate: should handle pagination across multiple pages', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
80
|
const piwikApi = jest.fn();
|
|
86
|
-
|
|
87
|
-
piwikApi
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
// Mock first call to return exactly 10 visits (triggers pagination)
|
|
82
|
+
piwikApi
|
|
83
|
+
.mockImplementationOnce((options, cb) => {
|
|
84
|
+
const visits = Array.from({ length: 10 }, (_, i) => (Object.assign(Object.assign({}, matomoVisit), { idVisit: 200 + i })));
|
|
85
|
+
cb(null, visits);
|
|
86
|
+
})
|
|
87
|
+
// Mock second call to return 5 visits (stops pagination)
|
|
88
|
+
.mockImplementationOnce((options, cb) => {
|
|
89
|
+
const visits = Array.from({ length: 5 }, (_, i) => (Object.assign(Object.assign({}, matomoVisit), { idVisit: 300 + i })));
|
|
90
|
+
cb(null, visits);
|
|
90
91
|
});
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
// Mock database query for record count
|
|
93
|
+
pool.query.mockResolvedValue({ rows: [], rowCount: 0 });
|
|
94
|
+
const result = yield importDate(piwikApi, TEST_DATE);
|
|
95
|
+
// Should make exactly 2 API calls due to pagination
|
|
93
96
|
expect(piwikApi.mock.calls.length).toEqual(2);
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
expect(piwikApi.mock.calls[1][0]).
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
expect(
|
|
117
|
-
|
|
97
|
+
// First call should have offset 0
|
|
98
|
+
expect(piwikApi.mock.calls[0][0]).toMatchObject({
|
|
99
|
+
date: '2023-04-15',
|
|
100
|
+
filter_limit: 10,
|
|
101
|
+
filter_offset: 0,
|
|
102
|
+
filter_sort_order: 'asc',
|
|
103
|
+
idSite: '42',
|
|
104
|
+
method: 'Live.getLastVisitsDetails',
|
|
105
|
+
period: 'day'
|
|
106
|
+
});
|
|
107
|
+
// Second call should have offset 10
|
|
108
|
+
expect(piwikApi.mock.calls[1][0]).toMatchObject({
|
|
109
|
+
date: '2023-04-15',
|
|
110
|
+
filter_limit: 10,
|
|
111
|
+
filter_offset: 10,
|
|
112
|
+
filter_sort_order: 'asc',
|
|
113
|
+
idSite: '42',
|
|
114
|
+
method: 'Live.getLastVisitsDetails',
|
|
115
|
+
period: 'day'
|
|
116
|
+
});
|
|
117
|
+
// Should process all events from both pages
|
|
118
|
+
// 15 visits total × 3 actionDetails each = 45 events
|
|
119
|
+
expect(result.length).toEqual(45);
|
|
120
|
+
// Verify database queries: 1 count query + (45 events × 1 query per event)
|
|
121
|
+
// Note: Each event generates 1 database query for insertion
|
|
122
|
+
expect(queries.length).toEqual(1 + 45);
|
|
118
123
|
}));
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const importEvent_1 = require("../importEvent");
|
|
7
|
-
const visit_json_1 = __importDefault(require("./visit.json"));
|
|
1
|
+
import { getEventsFromMatomoVisit } from '../importEvent.js';
|
|
2
|
+
import matomoVisit from './visit.json';
|
|
8
3
|
process.env.MATOMO_SITE = '42';
|
|
9
4
|
process.env.PROJECT_NAME = 'some-project';
|
|
10
5
|
process.env.RESULTPERPAGE = '10';
|
|
11
6
|
test('getEventsFromMatomoVisit: should merge action events', () => {
|
|
12
|
-
const visits =
|
|
7
|
+
const visits = getEventsFromMatomoVisit(matomoVisit);
|
|
13
8
|
expect(visits).toMatchSnapshot();
|
|
14
9
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
2
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
3
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -8,15 +7,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
8
|
});
|
|
10
9
|
};
|
|
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 index_1 = __importDefault(require("../index"));
|
|
16
10
|
process.env.MATOMO_SITE = '42';
|
|
17
11
|
process.env.PROJECT_NAME = 'some-project';
|
|
18
12
|
process.env.RESULTPERPAGE = '10';
|
|
19
|
-
process.env.
|
|
13
|
+
delete process.env.INITIAL_OFFSET;
|
|
14
|
+
delete process.env.DESTINATION_TABLE;
|
|
15
|
+
delete process.env.STARTDATE;
|
|
16
|
+
// Clear STARTDATE to avoid conflicts with fake timers
|
|
20
17
|
const TEST_DATE = new Date(2023, 3, 1);
|
|
21
18
|
let queries = [];
|
|
22
19
|
let piwikApiCalls = [];
|
|
@@ -46,18 +43,22 @@ jest.mock('../PiwikClient', () => {
|
|
|
46
43
|
}
|
|
47
44
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
48
45
|
api(options, cb) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
// Import the visit data dynamically to avoid circular dependency
|
|
48
|
+
const { default: matomoVisit } = yield import('./visit.json');
|
|
49
|
+
const matomoVisits = [
|
|
50
|
+
Object.assign(Object.assign({}, matomoVisit), { idVisit: 123 }),
|
|
51
|
+
Object.assign(Object.assign({}, matomoVisit), { idVisit: 124 })
|
|
52
|
+
];
|
|
53
|
+
piwikApiCalls.push(options);
|
|
54
|
+
cb(null, matomoVisits);
|
|
55
|
+
});
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
58
|
return PiwikMock;
|
|
60
59
|
});
|
|
60
|
+
// Import after mocks are set up
|
|
61
|
+
import run from '../index';
|
|
61
62
|
beforeEach(() => {
|
|
62
63
|
queries = [];
|
|
63
64
|
piwikApiCalls = [];
|
|
@@ -67,12 +68,12 @@ afterEach(() => {
|
|
|
67
68
|
});
|
|
68
69
|
test('run: should fetch the latest 5 days on matomo', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
69
70
|
jest.useFakeTimers().setSystemTime(TEST_DATE.getTime());
|
|
70
|
-
yield (
|
|
71
|
+
yield run();
|
|
71
72
|
expect(piwikApiCalls).toMatchSnapshot();
|
|
72
73
|
}));
|
|
73
74
|
test('run: should fetch the latest event date if no date provided', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
75
|
jest.useFakeTimers().setSystemTime(TEST_DATE.getTime());
|
|
75
|
-
yield (
|
|
76
|
+
yield run();
|
|
76
77
|
expect(queries[0]).toMatchSnapshot();
|
|
77
78
|
}));
|
|
78
79
|
test('run: should run based on existing data if any', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -81,7 +82,9 @@ test('run: should run based on existing data if any', () => __awaiter(void 0, vo
|
|
|
81
82
|
}));
|
|
82
83
|
test('run: should run SQL queries', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
84
|
jest.useFakeTimers().setSystemTime(TEST_DATE.getTime());
|
|
84
|
-
yield (
|
|
85
|
+
yield run();
|
|
85
86
|
expect(queries).toMatchSnapshot();
|
|
86
|
-
|
|
87
|
+
// Updated expectation based on actual behavior with INITIAL_OFFSET=3 (5 days total: 3 days before + today + 1 day after)
|
|
88
|
+
// 5 days * (6 events per day + 1 count query per day) + 1 initial query for last event lookup
|
|
89
|
+
expect(queries.length).toEqual(1 + 5 * (6 + 1));
|
|
87
90
|
}));
|
package/dist/config.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
export const MATOMO_KEY = process.env.MATOMO_KEY || '';
|
|
2
|
+
export const MATOMO_URL = process.env.MATOMO_URL || 'https://matomo.fabrique.social.gouv.fr/';
|
|
3
|
+
export const MATOMO_SITE = process.env.MATOMO_SITE || 0;
|
|
4
|
+
export const PGDATABASE = process.env.PGDATABASE || '';
|
|
5
|
+
export const INITIAL_OFFSET = process.env.INITIAL_OFFSET || '3';
|
|
6
|
+
export const RESULTPERPAGE = process.env.RESULTPERPAGE || '500';
|
|
7
|
+
// We will create both a normal and a partitioned table (MATOMO_TABLE_NAME and PARTITIONED_MATOMO_TABLE_NAME)
|
|
8
|
+
// and use DESTINATION_TABLE to determine which one to write to.
|
|
9
|
+
export const DESTINATION_TABLE = process.env.DESTINATION_TABLE || 'matomo';
|
|
10
|
+
export const MATOMO_TABLE_NAME = process.env.MATOMO_TABLE_NAME || 'matomo';
|
|
11
|
+
export const PARTITIONED_MATOMO_TABLE_NAME = process.env.PARTITIONED_MATOMO_TABLE_NAME || 'matomo_partitioned';
|
package/dist/db.js
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
(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,
|
|
1
|
+
import startDebug from 'debug';
|
|
2
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
3
|
+
import pkg from 'pg';
|
|
4
|
+
const { Pool } = pkg;
|
|
5
|
+
import { PGDATABASE } from './config.js';
|
|
6
|
+
startDebug('db');
|
|
7
|
+
export const db = new Kysely({
|
|
8
|
+
dialect: new PostgresDialect({
|
|
9
|
+
pool: new Pool({
|
|
10
|
+
connectionString: PGDATABASE,
|
|
16
11
|
ssl: {
|
|
17
12
|
rejectUnauthorized: false
|
|
18
13
|
}
|