@socialgouv/matomo-postgres 2.2.0-beta.3 → 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 +198 -32
- package/dist/__tests__/importDate.test.js +41 -31
- package/dist/__tests__/run.test.js +19 -11
- package/dist/config.js +5 -2
- package/dist/importDate.js +1 -1
- package/dist/importEvent.js +123 -50
- package/dist/index.js +5 -16
- package/dist/migrate-down.js +1 -4
- package/dist/migrate-latest.js +39 -30
- package/dist/migrations/20250425-01-add-resolution.js +16 -4
- package/dist/migrations/20250715-01-weekly-partitioning.js +6 -4
- package/dist/migrations/20250908-01-convention-analysis-index.js +29 -0
- package/package.json +5 -6
package/README.md
CHANGED
|
@@ -2,46 +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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
### Local Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @socialgouv/matomo-postgres
|
|
30
|
+
# or
|
|
31
|
+
yarn add @socialgouv/matomo-postgres
|
|
32
|
+
```
|
|
33
|
+
|
|
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**:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
docker-compose up
|
|
121
|
+
```
|
|
122
|
+
|
|
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
|
|
40
158
|
```
|
|
41
159
|
|
|
42
|
-
|
|
160
|
+
## 🗄️ Database Migrations
|
|
43
161
|
|
|
44
|
-
|
|
162
|
+
Database schema is managed through Kysely migrations located in `./src/migrations/`:
|
|
45
163
|
|
|
46
|
-
|
|
164
|
+
Migrations run automatically on each `yarn start` to ensure schema compatibility.
|
|
47
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
|
+
```
|
|
@@ -76,38 +76,48 @@ test('importDate: should import given date', () => __awaiter(void 0, void 0, voi
|
|
|
76
76
|
`);
|
|
77
77
|
expect(queries.length).toEqual(1 + matomoVisit.actionDetails.length * 2);
|
|
78
78
|
}));
|
|
79
|
-
test('importDate: should
|
|
79
|
+
test('importDate: should handle pagination across multiple pages', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
80
80
|
const piwikApi = jest.fn();
|
|
81
|
-
|
|
82
|
-
piwikApi
|
|
83
|
-
|
|
84
|
-
|
|
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);
|
|
85
91
|
});
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
88
96
|
expect(piwikApi.mock.calls.length).toEqual(2);
|
|
89
|
-
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
expect(piwikApi.mock.calls[1][0]).
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
expect(
|
|
112
|
-
|
|
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);
|
|
113
123
|
}));
|
|
@@ -7,11 +7,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import run from '../index';
|
|
11
10
|
process.env.MATOMO_SITE = '42';
|
|
12
11
|
process.env.PROJECT_NAME = 'some-project';
|
|
13
12
|
process.env.RESULTPERPAGE = '10';
|
|
14
|
-
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
|
|
15
17
|
const TEST_DATE = new Date(2023, 3, 1);
|
|
16
18
|
let queries = [];
|
|
17
19
|
let piwikApiCalls = [];
|
|
@@ -41,18 +43,22 @@ jest.mock('../PiwikClient', () => {
|
|
|
41
43
|
}
|
|
42
44
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
43
45
|
api(options, cb) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
});
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
return PiwikMock;
|
|
55
59
|
});
|
|
60
|
+
// Import after mocks are set up
|
|
61
|
+
import run from '../index';
|
|
56
62
|
beforeEach(() => {
|
|
57
63
|
queries = [];
|
|
58
64
|
piwikApiCalls = [];
|
|
@@ -78,5 +84,7 @@ test('run: should run SQL queries', () => __awaiter(void 0, void 0, void 0, func
|
|
|
78
84
|
jest.useFakeTimers().setSystemTime(TEST_DATE.getTime());
|
|
79
85
|
yield run();
|
|
80
86
|
expect(queries).toMatchSnapshot();
|
|
81
|
-
|
|
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));
|
|
82
90
|
}));
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,10 @@ export const MATOMO_KEY = process.env.MATOMO_KEY || '';
|
|
|
2
2
|
export const MATOMO_URL = process.env.MATOMO_URL || 'https://matomo.fabrique.social.gouv.fr/';
|
|
3
3
|
export const MATOMO_SITE = process.env.MATOMO_SITE || 0;
|
|
4
4
|
export const PGDATABASE = process.env.PGDATABASE || '';
|
|
5
|
-
export const DESTINATION_TABLE = process.env.DESTINATION_TABLE || 'matomo';
|
|
6
|
-
export const MATOMO_TABLE_NAME = process.env.MATOMO_TABLE_NAME || 'matomo';
|
|
7
5
|
export const INITIAL_OFFSET = process.env.INITIAL_OFFSET || '3';
|
|
8
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/importDate.js
CHANGED
|
@@ -30,7 +30,7 @@ const getRecordsCount = (date) => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
30
30
|
return count;
|
|
31
31
|
});
|
|
32
32
|
/** import all event from givent date */
|
|
33
|
-
export const importDate = (
|
|
33
|
+
export const importDate = (piwikApi_1, date_1, ...args_1) => __awaiter(void 0, [piwikApi_1, date_1, ...args_1], void 0, function* (piwikApi, date, filterOffset = 0) {
|
|
34
34
|
const limit = parseInt(RESULTPERPAGE);
|
|
35
35
|
const offset = filterOffset || (yield getRecordsCount(isoDate(date)));
|
|
36
36
|
if (!offset) {
|
package/dist/importEvent.js
CHANGED
|
@@ -18,55 +18,127 @@ import { db } from './db.js';
|
|
|
18
18
|
*/
|
|
19
19
|
export const importEvent = (event) => __awaiter(void 0, void 0, void 0, function* () {
|
|
20
20
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14;
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
21
|
+
// Build a sanitized, typed data object to reduce drift and ensure defaults in one place
|
|
22
|
+
const eventData = {
|
|
23
|
+
action_id: (_a = event.action_id) !== null && _a !== void 0 ? _a : '',
|
|
24
|
+
action_timestamp: event.action_timestamp
|
|
25
|
+
? new Date(event.action_timestamp)
|
|
26
|
+
: new Date(),
|
|
27
|
+
idsite: (_b = event.idsite) !== null && _b !== void 0 ? _b : '',
|
|
28
|
+
idvisit: (_c = event.idvisit) !== null && _c !== void 0 ? _c : '',
|
|
29
|
+
actions: (_d = event.actions) !== null && _d !== void 0 ? _d : null,
|
|
30
|
+
country: (_e = event.country) !== null && _e !== void 0 ? _e : null,
|
|
31
|
+
region: (_f = event.region) !== null && _f !== void 0 ? _f : null,
|
|
32
|
+
city: (_g = event.city) !== null && _g !== void 0 ? _g : null,
|
|
33
|
+
operatingsystemname: (_h = event.operatingsystemname) !== null && _h !== void 0 ? _h : null,
|
|
34
|
+
devicemodel: (_j = event.devicemodel) !== null && _j !== void 0 ? _j : null,
|
|
35
|
+
devicebrand: (_k = event.devicebrand) !== null && _k !== void 0 ? _k : null,
|
|
36
|
+
visitduration: (_l = event.visitduration) !== null && _l !== void 0 ? _l : null,
|
|
37
|
+
dayssincefirstvisit: (_m = event.dayssincefirstvisit) !== null && _m !== void 0 ? _m : null,
|
|
38
|
+
visitortype: (_o = event.visitortype) !== null && _o !== void 0 ? _o : null,
|
|
39
|
+
sitename: (_p = event.sitename) !== null && _p !== void 0 ? _p : null,
|
|
40
|
+
userid: (_q = event.userid) !== null && _q !== void 0 ? _q : null,
|
|
41
|
+
serverdateprettyfirstaction: event.serverdateprettyfirstaction
|
|
42
|
+
? new Date(event.serverdateprettyfirstaction)
|
|
43
|
+
: null,
|
|
44
|
+
action_type: (_r = event.action_type) !== null && _r !== void 0 ? _r : '',
|
|
45
|
+
action_eventcategory: (_s = event.action_eventcategory) !== null && _s !== void 0 ? _s : '',
|
|
46
|
+
action_eventaction: (_t = event.action_eventaction) !== null && _t !== void 0 ? _t : '',
|
|
47
|
+
action_eventname: (_u = event.action_eventname) !== null && _u !== void 0 ? _u : '',
|
|
48
|
+
action_eventvalue: event.action_eventvalue
|
|
49
|
+
? Number(event.action_eventvalue)
|
|
50
|
+
: 0,
|
|
51
|
+
action_timespent: (_v = event.action_timespent) !== null && _v !== void 0 ? _v : '0',
|
|
52
|
+
usercustomproperties: (_w = event.usercustomproperties) !== null && _w !== void 0 ? _w : null,
|
|
53
|
+
usercustomdimensions: (_x = event.usercustomdimensions) !== null && _x !== void 0 ? _x : null,
|
|
54
|
+
dimension1: (_y = event.dimension1) !== null && _y !== void 0 ? _y : null,
|
|
55
|
+
dimension2: (_z = event.dimension2) !== null && _z !== void 0 ? _z : null,
|
|
56
|
+
dimension3: (_0 = event.dimension3) !== null && _0 !== void 0 ? _0 : null,
|
|
57
|
+
dimension4: (_1 = event.dimension4) !== null && _1 !== void 0 ? _1 : null,
|
|
58
|
+
dimension5: (_2 = event.dimension5) !== null && _2 !== void 0 ? _2 : null,
|
|
59
|
+
dimension6: (_3 = event.dimension6) !== null && _3 !== void 0 ? _3 : null,
|
|
60
|
+
dimension7: (_4 = event.dimension7) !== null && _4 !== void 0 ? _4 : null,
|
|
61
|
+
dimension8: (_5 = event.dimension8) !== null && _5 !== void 0 ? _5 : null,
|
|
62
|
+
dimension9: (_6 = event.dimension9) !== null && _6 !== void 0 ? _6 : null,
|
|
63
|
+
dimension10: (_7 = event.dimension10) !== null && _7 !== void 0 ? _7 : null,
|
|
64
|
+
action_url: (_8 = event.action_url) !== null && _8 !== void 0 ? _8 : null,
|
|
65
|
+
sitesearchkeyword: (_9 = event.sitesearchkeyword) !== null && _9 !== void 0 ? _9 : null,
|
|
66
|
+
action_title: (_10 = event.action_title) !== null && _10 !== void 0 ? _10 : null,
|
|
67
|
+
visitorid: (_11 = event.visitorid) !== null && _11 !== void 0 ? _11 : null,
|
|
68
|
+
referrertype: (_12 = event.referrertype) !== null && _12 !== void 0 ? _12 : null,
|
|
69
|
+
referrername: (_13 = event.referrername) !== null && _13 !== void 0 ? _13 : null,
|
|
70
|
+
resolution: (_14 = event.resolution) !== null && _14 !== void 0 ? _14 : null
|
|
71
|
+
};
|
|
72
|
+
// Minimal runtime validation for required fields
|
|
73
|
+
if (!eventData.action_id || eventData.action_id.trim().length === 0) {
|
|
74
|
+
throw new Error('importEvent(): action_id is required and cannot be empty');
|
|
75
|
+
}
|
|
76
|
+
if (!(eventData.action_timestamp instanceof Date) ||
|
|
77
|
+
isNaN(eventData.action_timestamp.getTime())) {
|
|
78
|
+
throw new Error('importEvent(): action_timestamp is invalid');
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
// Keep the stored procedure but centralize mapping to avoid parameter mis-ordering
|
|
82
|
+
yield sql `
|
|
83
|
+
SELECT insert_into_matomo_partitioned(
|
|
84
|
+
${eventData.action_id},
|
|
85
|
+
${eventData.action_timestamp},
|
|
86
|
+
${eventData.idsite},
|
|
87
|
+
${eventData.idvisit},
|
|
88
|
+
${eventData.actions},
|
|
89
|
+
${eventData.country},
|
|
90
|
+
${eventData.region},
|
|
91
|
+
${eventData.city},
|
|
92
|
+
${eventData.operatingsystemname},
|
|
93
|
+
${eventData.devicemodel},
|
|
94
|
+
${eventData.devicebrand},
|
|
95
|
+
${eventData.visitduration},
|
|
96
|
+
${eventData.dayssincefirstvisit},
|
|
97
|
+
${eventData.visitortype},
|
|
98
|
+
${eventData.sitename},
|
|
99
|
+
${eventData.userid},
|
|
100
|
+
${eventData.serverdateprettyfirstaction},
|
|
101
|
+
${eventData.action_type},
|
|
102
|
+
${eventData.action_eventcategory},
|
|
103
|
+
${eventData.action_eventaction},
|
|
104
|
+
${eventData.action_eventname},
|
|
105
|
+
${eventData.action_eventvalue},
|
|
106
|
+
${eventData.action_timespent},
|
|
107
|
+
${eventData.usercustomproperties},
|
|
108
|
+
${eventData.usercustomdimensions},
|
|
109
|
+
${eventData.dimension1},
|
|
110
|
+
${eventData.dimension2},
|
|
111
|
+
${eventData.dimension3},
|
|
112
|
+
${eventData.dimension4},
|
|
113
|
+
${eventData.dimension5},
|
|
114
|
+
${eventData.dimension6},
|
|
115
|
+
${eventData.dimension7},
|
|
116
|
+
${eventData.dimension8},
|
|
117
|
+
${eventData.dimension9},
|
|
118
|
+
${eventData.dimension10},
|
|
119
|
+
${eventData.action_url},
|
|
120
|
+
${eventData.sitesearchkeyword},
|
|
121
|
+
${eventData.action_title},
|
|
122
|
+
${eventData.visitorid},
|
|
123
|
+
${eventData.referrertype},
|
|
124
|
+
${eventData.referrername},
|
|
125
|
+
${eventData.resolution}
|
|
126
|
+
)
|
|
127
|
+
`.execute(db);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// Add context for troubleshooting
|
|
131
|
+
const minimalContext = {
|
|
132
|
+
action_id: eventData.action_id,
|
|
133
|
+
action_timestamp: eventData.action_timestamp,
|
|
134
|
+
idsite: eventData.idsite,
|
|
135
|
+
idvisit: eventData.idvisit
|
|
136
|
+
};
|
|
137
|
+
console.error('importEvent(): failed to insert event', minimalContext);
|
|
138
|
+
// Log error details but avoid exposing sensitive information
|
|
139
|
+
console.error('importEvent(): error', err instanceof Error ? err.message : 'Unknown error');
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
70
142
|
});
|
|
71
143
|
const matomoProps = [
|
|
72
144
|
'idSite',
|
|
@@ -103,9 +175,10 @@ const actionProps = {
|
|
|
103
175
|
};
|
|
104
176
|
export const getEventsFromMatomoVisit = (matomoVisit) => {
|
|
105
177
|
return matomoVisit.actionDetails.map((actionDetail, actionIndex) => {
|
|
178
|
+
var _a;
|
|
106
179
|
const usercustomproperties = {};
|
|
107
180
|
for (let k = 1; k < 10; k++) {
|
|
108
|
-
const property = actionDetail.customVariables
|
|
181
|
+
const property = (_a = actionDetail.customVariables) === null || _a === void 0 ? void 0 : _a[k];
|
|
109
182
|
if (!property)
|
|
110
183
|
continue; // max 10 custom variables
|
|
111
184
|
//@ts-expect-error implicit any type
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { eachDayOfInterval } from 'date-fns';
|
|
|
11
11
|
import startDebug from 'debug';
|
|
12
12
|
import { sql } from 'kysely';
|
|
13
13
|
import pAll from 'p-all';
|
|
14
|
-
import { DESTINATION_TABLE, INITIAL_OFFSET, MATOMO_KEY, MATOMO_SITE, MATOMO_URL
|
|
14
|
+
import { DESTINATION_TABLE, INITIAL_OFFSET, MATOMO_KEY, MATOMO_SITE, MATOMO_URL } from './config.js';
|
|
15
15
|
import { db } from './db.js';
|
|
16
16
|
import { importDate } from './importDate.js';
|
|
17
17
|
import PiwikClient from './PiwikClient.js';
|
|
@@ -35,10 +35,6 @@ function run(date) {
|
|
|
35
35
|
referenceDate = new Date(date);
|
|
36
36
|
console.log(`✅ Using provided date parameter: ${referenceDate.toISOString()}`);
|
|
37
37
|
}
|
|
38
|
-
if (!referenceDate && process.env.STARTDATE) {
|
|
39
|
-
referenceDate = new Date(process.env.STARTDATE);
|
|
40
|
-
console.log(`✅ Using STARTDATE environment variable: ${referenceDate.toISOString()}`);
|
|
41
|
-
}
|
|
42
38
|
if (!referenceDate) {
|
|
43
39
|
console.log(`🔍 Looking for last event in database...`);
|
|
44
40
|
referenceDate = yield findLastEventInMatomo(db);
|
|
@@ -49,6 +45,10 @@ function run(date) {
|
|
|
49
45
|
console.log(`ℹ️ No previous events found in database`);
|
|
50
46
|
}
|
|
51
47
|
}
|
|
48
|
+
if (!referenceDate && process.env.STARTDATE) {
|
|
49
|
+
referenceDate = new Date(process.env.STARTDATE);
|
|
50
|
+
console.log(`✅ Using STARTDATE environment variable: ${referenceDate.toISOString()}`);
|
|
51
|
+
}
|
|
52
52
|
if (!referenceDate) {
|
|
53
53
|
referenceDate = new Date(new Date().getTime() - +INITIAL_OFFSET * 24 * 60 * 60 * 1000);
|
|
54
54
|
console.log(`✅ Using default offset (${INITIAL_OFFSET} days ago): ${referenceDate.toISOString()}`);
|
|
@@ -95,14 +95,3 @@ function findLastEventInMatomo(db) {
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
export default run;
|
|
98
|
-
(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
-
if (!MATOMO_SITE)
|
|
100
|
-
return console.error('Missing env MATOMO_SITE');
|
|
101
|
-
if (!MATOMO_KEY)
|
|
102
|
-
return console.error('Missing env MATOMO_KEY');
|
|
103
|
-
if (!PGDATABASE)
|
|
104
|
-
return console.error('Missing env PGDATABASE');
|
|
105
|
-
yield run();
|
|
106
|
-
debug('run finished');
|
|
107
|
-
db.destroy();
|
|
108
|
-
}))();
|
package/dist/migrate-down.js
CHANGED
|
@@ -10,9 +10,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { promises as fs } from 'fs';
|
|
11
11
|
import { FileMigrationProvider, Migrator } from 'kysely';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
const __dirname = path.dirname(__filename);
|
|
16
13
|
import { db } from './db.js';
|
|
17
14
|
function migrateDown() {
|
|
18
15
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -21,7 +18,7 @@ function migrateDown() {
|
|
|
21
18
|
provider: new FileMigrationProvider({
|
|
22
19
|
fs,
|
|
23
20
|
path,
|
|
24
|
-
migrationFolder:
|
|
21
|
+
migrationFolder: path.join(path.dirname(new URL(import.meta.url).pathname), 'migrations')
|
|
25
22
|
})
|
|
26
23
|
});
|
|
27
24
|
const { error, results } = yield migrator.migrateDown();
|
package/dist/migrate-latest.js
CHANGED
|
@@ -10,49 +10,58 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { promises as fs } from 'fs';
|
|
11
11
|
import { FileMigrationProvider, Migrator } from 'kysely';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
const __dirname = path.dirname(__filename);
|
|
16
13
|
import { MATOMO_TABLE_NAME } from './config.js';
|
|
17
14
|
import { db } from './db.js';
|
|
18
15
|
function migrateToLatest() {
|
|
19
16
|
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
17
|
+
console.log(`Starting migrate to latest`);
|
|
18
|
+
try {
|
|
19
|
+
const migrator = new Migrator({
|
|
20
|
+
db,
|
|
21
|
+
provider: new FileMigrationProvider({
|
|
22
|
+
fs,
|
|
23
|
+
path,
|
|
24
|
+
migrationFolder: path.join(path.dirname(new URL(import.meta.url).pathname), 'migrations')
|
|
25
|
+
}),
|
|
26
|
+
// allow to have mutliple migratable instances in a single schema
|
|
27
|
+
migrationTableName: `${MATOMO_TABLE_NAME}_migration`,
|
|
28
|
+
migrationLockTableName: `${MATOMO_TABLE_NAME}_migration_lock`
|
|
29
|
+
});
|
|
30
|
+
const { error, results } = yield migrator.migrateToLatest();
|
|
31
|
+
results === null || results === void 0 ? void 0 : results.forEach((it) => {
|
|
32
|
+
if (it.status === 'Success') {
|
|
33
|
+
console.log(`migration "${it.migrationName}" was executed successfully`);
|
|
34
|
+
}
|
|
35
|
+
else if (it.status === 'Error') {
|
|
36
|
+
console.error(`failed to execute migration "${it.migrationName}"`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
if (error) {
|
|
40
|
+
console.error('failed to migrate');
|
|
41
|
+
console.error(error);
|
|
42
|
+
process.exit(1);
|
|
35
43
|
}
|
|
36
|
-
else if (
|
|
37
|
-
console.error(`failed to execute migration "${it.migrationName}"`);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
if (error) {
|
|
41
|
-
console.error('failed to migrate');
|
|
42
|
-
console.error(error);
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
if (!(results === null || results === void 0 ? void 0 : results.length)) {
|
|
44
|
+
else if (!(results === null || results === void 0 ? void 0 : results.length)) {
|
|
47
45
|
console.log('No migration to run');
|
|
48
46
|
}
|
|
49
47
|
}
|
|
48
|
+
catch (uncaughtError) {
|
|
49
|
+
console.error('UNCAUGHT ERROR during migration:');
|
|
50
|
+
console.error('Error message:', uncaughtError instanceof Error
|
|
51
|
+
? uncaughtError.message
|
|
52
|
+
: String(uncaughtError));
|
|
53
|
+
console.error('Error stack:', uncaughtError instanceof Error
|
|
54
|
+
? uncaughtError.stack
|
|
55
|
+
: 'No stack trace available');
|
|
56
|
+
console.error('Full error object:', uncaughtError);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
50
59
|
});
|
|
51
60
|
}
|
|
52
61
|
export default migrateToLatest;
|
|
53
62
|
export function startMigration() {
|
|
54
63
|
return __awaiter(this, void 0, void 0, function* () {
|
|
55
64
|
yield migrateToLatest();
|
|
56
|
-
|
|
65
|
+
// Don't destroy the db connection here since the main application will need it
|
|
57
66
|
});
|
|
58
67
|
}
|
|
@@ -7,13 +7,25 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
import { sql } from 'kysely';
|
|
10
11
|
const MATOMO_TABLE_NAME = process.env.MATOMO_TABLE_NAME || 'matomo';
|
|
11
12
|
export function up(db) {
|
|
12
13
|
return __awaiter(this, void 0, void 0, function* () {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// Check if the column already exists before trying to add it
|
|
15
|
+
const columnExists = yield sql `
|
|
16
|
+
SELECT EXISTS (
|
|
17
|
+
SELECT 1
|
|
18
|
+
FROM information_schema.columns
|
|
19
|
+
WHERE table_name = ${MATOMO_TABLE_NAME}
|
|
20
|
+
AND column_name = 'resolution'
|
|
21
|
+
) as exists
|
|
22
|
+
`.execute(db);
|
|
23
|
+
if (!columnExists.rows[0].exists) {
|
|
24
|
+
yield db.schema
|
|
25
|
+
.alterTable(MATOMO_TABLE_NAME)
|
|
26
|
+
.addColumn('resolution', 'text')
|
|
27
|
+
.execute();
|
|
28
|
+
}
|
|
17
29
|
});
|
|
18
30
|
}
|
|
19
31
|
export function down(db) {
|
|
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { sql } from 'kysely';
|
|
11
|
-
|
|
11
|
+
import { PARTITIONED_MATOMO_TABLE_NAME } from 'src/config';
|
|
12
12
|
export function up(db) {
|
|
13
13
|
return __awaiter(this, void 0, void 0, function* () {
|
|
14
14
|
// First, create the partitioned table structure as a partitioned table
|
|
@@ -162,7 +162,7 @@ export function up(db) {
|
|
|
162
162
|
)
|
|
163
163
|
RETURNS void
|
|
164
164
|
LANGUAGE plpgsql
|
|
165
|
-
SECURITY
|
|
165
|
+
SECURITY INVOKER
|
|
166
166
|
AS $$
|
|
167
167
|
BEGIN
|
|
168
168
|
-- Ensure partition exists for the given timestamp
|
|
@@ -347,8 +347,10 @@ export function up(db) {
|
|
|
347
347
|
export function down(db) {
|
|
348
348
|
return __awaiter(this, void 0, void 0, function* () {
|
|
349
349
|
// Drop trigger and function
|
|
350
|
-
|
|
351
|
-
yield sql `DROP
|
|
350
|
+
const trigger_name = `${PARTITIONED_MATOMO_TABLE_NAME}_auto_partition`;
|
|
351
|
+
yield sql `DROP TRIGGER IF EXISTS ${sql.id(trigger_name)} ON ${sql.id(PARTITIONED_MATOMO_TABLE_NAME)}`.execute(db);
|
|
352
|
+
const function_name = `${PARTITIONED_MATOMO_TABLE_NAME}_partition_trigger`;
|
|
353
|
+
yield sql `DROP FUNCTION IF EXISTS ${sql.id(function_name)}()`.execute(db);
|
|
352
354
|
yield sql `DROP FUNCTION IF EXISTS create_weekly_partition_if_not_exists(text, timestamptz)`.execute(db);
|
|
353
355
|
yield sql `DROP FUNCTION IF EXISTS insert_into_matomo_partitioned`.execute(db);
|
|
354
356
|
// Drop the partitioned table (this will also drop all partitions)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { sql } from 'kysely';
|
|
11
|
+
const PARTITIONED_MATOMO_TABLE_NAME = process.env.PARTITIONED_MATOMO_TABLE_NAME || 'matomo_partitioned';
|
|
12
|
+
export function up(db) {
|
|
13
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
// Create conditional index for convention collective analysis
|
|
15
|
+
yield sql `
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_convention_analysis_matomo_partitioned
|
|
17
|
+
ON ${sql.id(PARTITIONED_MATOMO_TABLE_NAME)} (action_type, action_url, action_timestamp)
|
|
18
|
+
WHERE action_url LIKE 'https://code.travail.gouv.fr/convention-collective/%'
|
|
19
|
+
`.execute(db);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function down(db) {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
// Drop the conditional index
|
|
25
|
+
yield sql `
|
|
26
|
+
DROP INDEX IF EXISTS idx_convention_analysis_matomo_partitioned
|
|
27
|
+
`.execute(db);
|
|
28
|
+
});
|
|
29
|
+
}
|
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": "2.2.
|
|
4
|
+
"version": "2.2.1",
|
|
5
5
|
"types": "types/index.d.ts",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@eslint/js": "^9.31.0",
|
|
43
43
|
"@types/debug": "^4.1.7",
|
|
44
44
|
"@types/jest": "^29.4.0",
|
|
45
|
-
"@types/node": "^
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
46
|
"@types/pg": "^8.6.6",
|
|
47
47
|
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
|
48
48
|
"@typescript-eslint/parser": "^8.37.0",
|
|
@@ -51,11 +51,10 @@
|
|
|
51
51
|
"eslint-plugin-prettier": "^5.5.1",
|
|
52
52
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
53
53
|
"globals": "^16.3.0",
|
|
54
|
-
"jest": "^29.
|
|
55
|
-
"knip": "^5.61.3",
|
|
54
|
+
"jest": "^29.7.0",
|
|
56
55
|
"prettier": "^3.6.2",
|
|
57
|
-
"ts-jest": "^29.
|
|
56
|
+
"ts-jest": "^29.4.1",
|
|
58
57
|
"ts-node": "^10.9.1",
|
|
59
|
-
"typescript": "^
|
|
58
|
+
"typescript": "^5.0.0"
|
|
60
59
|
}
|
|
61
60
|
}
|