@socialgouv/matomo-postgres 2.2.0 → 2.2.2

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 CHANGED
@@ -2,45 +2,212 @@
2
2
 
3
3
  ![header](./header.png)
4
4
 
5
- Extract matomo data from [`Live.getLastVisitsDetails`](https://developer.matomo.org/api-reference/reporting-api) API and push events and visits informations to Postgres.
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
- ## Usage
7
+ ## ✨ Features
8
8
 
9
- Run the following job with correct environment variables.
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
- ```sh
18
+ ## 🚀 Quick Start
19
+
20
+ ### Global Installation
21
+
22
+ ```bash
12
23
  npx @socialgouv/matomo-postgres
13
24
  ```
14
25
 
15
- ### Environment variables Deployment
16
-
17
- | name | value |
18
- | ----------------- | -------------------------------------------------------- |
19
- | MATOMO_KEY\* | matomo api token |
20
- | MATOMO_SITE\* | matomo site id |
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
- Use `yarn test -u` to update the snapshots
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
- ## Database migrations
119
+ ```bash
120
+ docker-compose up
121
+ ```
45
122
 
46
- `yarn migrate` is run on each `yarn start` with Kysely migrations at [./src/migrations](./src/migrations/)
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 migrate from '../dist/migrate-latest'
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 migrate()
9
+ await startMigration()
10
10
  console.log(`\nStarting import\n`)
11
11
  await run(date)
12
12
  db.destroy()
13
13
  }
14
14
 
15
- if (require.main === module) {
16
- const date =
17
- (process.argv[process.argv.length - 1].match(/^\d\d\d\d-\d\d-\d\d$/) &&
18
- process.argv[process.argv.length - 1]) ||
19
- ''
20
- console.log(`\nRunning @socialgouv/matomo-postgres ${date}\n`)
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)
@@ -1,33 +1,8 @@
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
- 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
- const pg_1 = require("pg");
21
- const importDate_1 = require("../importDate");
22
- const visit_json_1 = __importDefault(require("./visit.json"));
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 pg_1.Pool();
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({}, visit_json_1.default), { idVisit: 123 }),
57
- Object.assign(Object.assign({}, visit_json_1.default), { idVisit: 124 })
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 (0, importDate_1.importDate)(piwikApi, TEST_DATE);
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 + visit_json_1.default.actionDetails.length * 2);
77
+ expect(queries.length).toEqual(1 + matomoVisit.actionDetails.length * 2);
83
78
  }));
84
- test('importDate: should paginate matomo API calls and produce 46 queries', () => __awaiter(void 0, void 0, void 0, function* () {
79
+ test('importDate: should handle pagination across multiple pages', () => __awaiter(void 0, void 0, void 0, function* () {
85
80
  const piwikApi = jest.fn();
86
- let calls = 0;
87
- piwikApi.mockImplementation((options, cb) => {
88
- cb(null, Array.from({ length: calls ? 5 : 10 }, (k, _v) => (Object.assign(Object.assign({}, visit_json_1.default), { idVisit: k }))));
89
- calls++;
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
- pool.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
92
- yield (0, importDate_1.importDate)(piwikApi, TEST_DATE);
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
- expect(piwikApi.mock.calls[0][0]).toMatchInlineSnapshot(`
95
- {
96
- "date": "2023-04-15",
97
- "filter_limit": 10,
98
- "filter_offset": 0,
99
- "filter_sort_order": "asc",
100
- "idSite": "42",
101
- "method": "Live.getLastVisitsDetails",
102
- "period": "day",
103
- }
104
- `);
105
- expect(piwikApi.mock.calls[1][0]).toMatchInlineSnapshot(`
106
- {
107
- "date": "2023-04-15",
108
- "filter_limit": 10,
109
- "filter_offset": 10,
110
- "filter_sort_order": "asc",
111
- "idSite": "42",
112
- "method": "Live.getLastVisitsDetails",
113
- "period": "day",
114
- }
115
- `);
116
- expect(queries.length).toEqual(1 + visit_json_1.default.actionDetails.length * 15);
117
- expect(queries).toMatchSnapshot();
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
- "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
- 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 = (0, importEvent_1.getEventsFromMatomoVisit)(visit_json_1.default);
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.STARTDATE = '2023-03-27'; // Set a start date that's before our test date
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
- piwikApiCalls.push(options);
50
- // Load the visit data dynamically to avoid hoisting issues
51
- const matomoVisit = jest.requireActual('./visit.json');
52
- const matomoVisits = [
53
- Object.assign(Object.assign({}, matomoVisit), { idVisit: 123 }),
54
- Object.assign(Object.assign({}, matomoVisit), { idVisit: 124 })
55
- ];
56
- cb(null, matomoVisits);
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 (0, index_1.default)();
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 (0, index_1.default)();
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 (0, index_1.default)();
85
+ yield run();
85
86
  expect(queries).toMatchSnapshot();
86
- expect(queries.length).toEqual(49); // Number of queries based on current implementation
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RESULTPERPAGE = exports.INITIAL_OFFSET = exports.MATOMO_TABLE_NAME = 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.MATOMO_TABLE_NAME = process.env.MATOMO_TABLE_NAME || 'matomo';
10
- exports.INITIAL_OFFSET = process.env.INITIAL_OFFSET || '3';
11
- exports.RESULTPERPAGE = process.env.RESULTPERPAGE || '500';
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
- "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 debug_1 = __importDefault(require("debug"));
8
- const kysely_1 = require("kysely");
9
- const pg_1 = require("pg");
10
- const config_1 = require("./config");
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
  }