@happyvertical/analytics 0.74.8
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/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/dist/chunks/ga4-6gyDPZRn.js +863 -0
- package/dist/chunks/ga4-6gyDPZRn.js.map +1 -0
- package/dist/chunks/matomo-Ds_oRmZ6.js +1043 -0
- package/dist/chunks/matomo-Ds_oRmZ6.js.map +1 -0
- package/dist/chunks/plausible-BxpNa6qF.js +479 -0
- package/dist/chunks/plausible-BxpNa6qF.js.map +1 -0
- package/dist/cli/claude-context.d.ts +3 -0
- package/dist/cli/claude-context.d.ts.map +1 -0
- package/dist/cli/claude-context.js +21 -0
- package/dist/cli/claude-context.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/shared/factory.d.ts +48 -0
- package/dist/shared/factory.d.ts.map +1 -0
- package/dist/shared/providers/ga4.d.ts +68 -0
- package/dist/shared/providers/ga4.d.ts.map +1 -0
- package/dist/shared/providers/matomo-admin.d.ts +56 -0
- package/dist/shared/providers/matomo-admin.d.ts.map +1 -0
- package/dist/shared/providers/matomo.d.ts +49 -0
- package/dist/shared/providers/matomo.d.ts.map +1 -0
- package/dist/shared/providers/plausible.d.ts +50 -0
- package/dist/shared/providers/plausible.d.ts.map +1 -0
- package/dist/shared/types.d.ts +1235 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/metadata.json +30 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
import { AnalyticsError, AuthenticationError, NotSupportedError, PropertyNotFoundError } from "../index.js";
|
|
2
|
+
const PROVIDER$1 = "matomo";
|
|
3
|
+
function stripTrailingSlash(value) {
|
|
4
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
5
|
+
}
|
|
6
|
+
function stripIndexPhpSuffix(value) {
|
|
7
|
+
return value.endsWith("/index.php") ? value.slice(0, -"/index.php".length) : value;
|
|
8
|
+
}
|
|
9
|
+
function normalizeMatomoBaseUrl(value) {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (!trimmed) {
|
|
12
|
+
throw new AnalyticsError(
|
|
13
|
+
"Matomo baseUrl is required",
|
|
14
|
+
"MATOMO_BASE_URL_REQUIRED",
|
|
15
|
+
PROVIDER$1
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return stripIndexPhpSuffix(stripTrailingSlash(trimmed));
|
|
19
|
+
}
|
|
20
|
+
function isJsonRecord(value) {
|
|
21
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function readString$1(record, key) {
|
|
24
|
+
const value = record[key];
|
|
25
|
+
if (typeof value === "string" && value.length > 0) {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "number") {
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
function readBoolean(record, key) {
|
|
34
|
+
const value = record[key];
|
|
35
|
+
if (typeof value === "boolean") return value;
|
|
36
|
+
if (typeof value === "string") {
|
|
37
|
+
if (value === "1" || value === "true") return true;
|
|
38
|
+
if (value === "0" || value === "false") return false;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "number") return value !== 0;
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
function readMatomoError(body) {
|
|
44
|
+
if (!isJsonRecord(body)) return void 0;
|
|
45
|
+
if (body.result !== "error") return void 0;
|
|
46
|
+
return readString$1(body, "message") ?? "Matomo returned an error";
|
|
47
|
+
}
|
|
48
|
+
class MatomoAdminTransport {
|
|
49
|
+
baseUrl;
|
|
50
|
+
tokenAuth;
|
|
51
|
+
timeout;
|
|
52
|
+
constructor(options) {
|
|
53
|
+
this.baseUrl = normalizeMatomoBaseUrl(options.baseUrl);
|
|
54
|
+
if (!options.tokenAuth) {
|
|
55
|
+
throw new AnalyticsError(
|
|
56
|
+
"Matomo tokenAuth is required",
|
|
57
|
+
"MATOMO_TOKEN_REQUIRED",
|
|
58
|
+
PROVIDER$1
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
this.tokenAuth = options.tokenAuth;
|
|
62
|
+
this.timeout = options.timeout;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Call a Matomo API method by name (e.g. `SitesManager.addSite`).
|
|
66
|
+
*
|
|
67
|
+
* `params` becomes the request body. Array values are encoded as
|
|
68
|
+
* `key[]=...&key[]=...`. Undefined and null values are dropped. The
|
|
69
|
+
* reserved keys `module`, `method`, `format`, and `token_auth` are
|
|
70
|
+
* controlled by the transport — any caller-supplied value for those
|
|
71
|
+
* keys is silently ignored so they cannot override the dispatch or
|
|
72
|
+
* the response format.
|
|
73
|
+
*/
|
|
74
|
+
async call(method, params) {
|
|
75
|
+
const body = new URLSearchParams();
|
|
76
|
+
for (const [key, value] of Object.entries(params)) {
|
|
77
|
+
if (RESERVED_PARAM_KEYS.has(key)) continue;
|
|
78
|
+
if (value === void 0 || value === null) continue;
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
for (const item of value) {
|
|
81
|
+
body.append(`${key}[]`, String(item));
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
body.set(key, String(value));
|
|
86
|
+
}
|
|
87
|
+
body.set("module", "API");
|
|
88
|
+
body.set("method", method);
|
|
89
|
+
body.set("format", "json");
|
|
90
|
+
body.set("token_auth", this.tokenAuth);
|
|
91
|
+
const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
|
|
92
|
+
const timeoutHandle = controller && this.timeout ? setTimeout(() => controller.abort(), this.timeout) : void 0;
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(`${this.baseUrl}/index.php`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
Accept: "application/json",
|
|
98
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
99
|
+
},
|
|
100
|
+
body,
|
|
101
|
+
signal: controller?.signal
|
|
102
|
+
});
|
|
103
|
+
const text = await response.text();
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw mapHttpError(response.status, tryParseJson(text), text);
|
|
106
|
+
}
|
|
107
|
+
const parsed = text.length > 0 ? parseJsonOrThrow(text) : void 0;
|
|
108
|
+
const errorMessage = readMatomoError(parsed);
|
|
109
|
+
if (errorMessage) {
|
|
110
|
+
throw mapApplicationError(errorMessage);
|
|
111
|
+
}
|
|
112
|
+
return parsed;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof AnalyticsError) throw error;
|
|
115
|
+
throw new AnalyticsError(
|
|
116
|
+
`Matomo admin request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
117
|
+
"MATOMO_REQUEST_FAILED",
|
|
118
|
+
PROVIDER$1
|
|
119
|
+
);
|
|
120
|
+
} finally {
|
|
121
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const RESERVED_PARAM_KEYS = /* @__PURE__ */ new Set([
|
|
126
|
+
"module",
|
|
127
|
+
"method",
|
|
128
|
+
"format",
|
|
129
|
+
"token_auth"
|
|
130
|
+
]);
|
|
131
|
+
function parseJsonOrThrow(text) {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(text);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const preview = text.slice(0, 200);
|
|
136
|
+
throw new AnalyticsError(
|
|
137
|
+
`Matomo returned an invalid JSON response${error instanceof Error ? `: ${error.message}` : ""}${preview ? ` (response preview: ${preview})` : ""}`,
|
|
138
|
+
"MATOMO_INVALID_RESPONSE",
|
|
139
|
+
PROVIDER$1
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function tryParseJson(text) {
|
|
144
|
+
if (!text) return void 0;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(text);
|
|
147
|
+
} catch {
|
|
148
|
+
return void 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function mapHttpError(status, body, text) {
|
|
152
|
+
if (status === 401 || status === 403) {
|
|
153
|
+
return new AuthenticationError(PROVIDER$1);
|
|
154
|
+
}
|
|
155
|
+
const message = isJsonRecord(body) && readString$1(body, "message") || text || `Matomo HTTP ${status}`;
|
|
156
|
+
return new AnalyticsError(message, `MATOMO_HTTP_${status}`, PROVIDER$1);
|
|
157
|
+
}
|
|
158
|
+
function mapApplicationError(message) {
|
|
159
|
+
if (message.includes("requires view access") || message.includes("requires admin access") || message.includes("requires Super User access") || message.includes("doesn't have access")) {
|
|
160
|
+
return new AuthenticationError(PROVIDER$1, message, "MATOMO_ACCESS_DENIED");
|
|
161
|
+
}
|
|
162
|
+
return new AnalyticsError(message, "MATOMO_API_ERROR", PROVIDER$1);
|
|
163
|
+
}
|
|
164
|
+
function siteFromRow(row) {
|
|
165
|
+
const id = readString$1(row, "idsite") ?? readString$1(row, "idSite");
|
|
166
|
+
if (!id) {
|
|
167
|
+
throw new AnalyticsError(
|
|
168
|
+
"Matomo did not return a site id",
|
|
169
|
+
"MATOMO_INVALID_RESPONSE",
|
|
170
|
+
PROVIDER$1
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
id,
|
|
175
|
+
name: readString$1(row, "name") ?? "",
|
|
176
|
+
url: readString$1(row, "main_url"),
|
|
177
|
+
timezone: readString$1(row, "timezone"),
|
|
178
|
+
currency: readString$1(row, "currency"),
|
|
179
|
+
provider: PROVIDER$1,
|
|
180
|
+
raw: row
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function userFromRow(row) {
|
|
184
|
+
const login = readString$1(row, "login");
|
|
185
|
+
if (!login) {
|
|
186
|
+
throw new AnalyticsError(
|
|
187
|
+
"Matomo did not return a user login",
|
|
188
|
+
"MATOMO_INVALID_RESPONSE",
|
|
189
|
+
PROVIDER$1
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
login,
|
|
194
|
+
email: readString$1(row, "email"),
|
|
195
|
+
isSuperUser: readBoolean(row, "superuser_access"),
|
|
196
|
+
provider: PROVIDER$1,
|
|
197
|
+
raw: row
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
class MatomoAdmin {
|
|
201
|
+
baseUrl;
|
|
202
|
+
timeout;
|
|
203
|
+
transport;
|
|
204
|
+
constructor(options) {
|
|
205
|
+
this.baseUrl = normalizeMatomoBaseUrl(options.baseUrl);
|
|
206
|
+
this.timeout = options.timeout;
|
|
207
|
+
this.transport = new MatomoAdminTransport({
|
|
208
|
+
...options,
|
|
209
|
+
baseUrl: this.baseUrl
|
|
210
|
+
});
|
|
211
|
+
this.createSite = this.createSite.bind(this);
|
|
212
|
+
this.listSites = this.listSites.bind(this);
|
|
213
|
+
this.getSite = this.getSite.bind(this);
|
|
214
|
+
this.updateSite = this.updateSite.bind(this);
|
|
215
|
+
this.deleteSite = this.deleteSite.bind(this);
|
|
216
|
+
this.createUser = this.createUser.bind(this);
|
|
217
|
+
this.getUser = this.getUser.bind(this);
|
|
218
|
+
this.deleteUser = this.deleteUser.bind(this);
|
|
219
|
+
this.setUserAccess = this.setUserAccess.bind(this);
|
|
220
|
+
this.verifyUserSiteAccess = this.verifyUserSiteAccess.bind(this);
|
|
221
|
+
this.verifyTokenSiteAccess = this.verifyTokenSiteAccess.bind(this);
|
|
222
|
+
this.mintUserToken = this.mintUserToken.bind(this);
|
|
223
|
+
this.health = this.health.bind(this);
|
|
224
|
+
}
|
|
225
|
+
cloneTransportWithToken(tokenAuth) {
|
|
226
|
+
return new MatomoAdminTransport({
|
|
227
|
+
baseUrl: this.baseUrl,
|
|
228
|
+
tokenAuth,
|
|
229
|
+
timeout: this.timeout
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Sites
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
async createSite(options) {
|
|
236
|
+
if (!options.urls || options.urls.length === 0) {
|
|
237
|
+
throw new AnalyticsError(
|
|
238
|
+
"Matomo createSite requires at least one URL",
|
|
239
|
+
"MATOMO_URLS_REQUIRED",
|
|
240
|
+
PROVIDER$1
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const response = await this.transport.call(
|
|
244
|
+
"SitesManager.addSite",
|
|
245
|
+
{
|
|
246
|
+
siteName: options.name,
|
|
247
|
+
urls: options.urls,
|
|
248
|
+
timezone: options.timezone,
|
|
249
|
+
currency: options.currency,
|
|
250
|
+
...options.raw ?? {}
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
const id = coerceSiteIdResponse(response);
|
|
254
|
+
if (!id) {
|
|
255
|
+
throw new AnalyticsError(
|
|
256
|
+
"Matomo did not return a site id",
|
|
257
|
+
"MATOMO_INVALID_RESPONSE",
|
|
258
|
+
PROVIDER$1
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const site = await this.getSite(id);
|
|
262
|
+
if (site) return { ...site, tenantId: options.tenantId };
|
|
263
|
+
return {
|
|
264
|
+
id,
|
|
265
|
+
name: options.name,
|
|
266
|
+
url: options.urls[0],
|
|
267
|
+
timezone: options.timezone,
|
|
268
|
+
currency: options.currency,
|
|
269
|
+
tenantId: options.tenantId,
|
|
270
|
+
provider: PROVIDER$1,
|
|
271
|
+
raw: response
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
async listSites() {
|
|
275
|
+
const response = await this.transport.call(
|
|
276
|
+
"SitesManager.getAllSites",
|
|
277
|
+
{}
|
|
278
|
+
);
|
|
279
|
+
if (!Array.isArray(response)) return [];
|
|
280
|
+
return response.filter(isJsonRecord).map((row) => siteFromRow(row));
|
|
281
|
+
}
|
|
282
|
+
async getSite(siteId) {
|
|
283
|
+
try {
|
|
284
|
+
const response = await this.transport.call(
|
|
285
|
+
"SitesManager.getSiteFromId",
|
|
286
|
+
{ idSite: siteId }
|
|
287
|
+
);
|
|
288
|
+
if (isJsonRecord(response)) {
|
|
289
|
+
return siteFromRow(response);
|
|
290
|
+
}
|
|
291
|
+
return void 0;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error instanceof AnalyticsError && isNotFoundSiteError(error)) {
|
|
294
|
+
return void 0;
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async updateSite(options) {
|
|
300
|
+
if (options.urls && options.urls.length === 0) {
|
|
301
|
+
throw new AnalyticsError(
|
|
302
|
+
"Matomo updateSite requires at least one URL when urls is provided",
|
|
303
|
+
"MATOMO_URLS_REQUIRED",
|
|
304
|
+
PROVIDER$1
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
await this.transport.call("SitesManager.updateSite", {
|
|
308
|
+
idSite: options.siteId,
|
|
309
|
+
siteName: options.name,
|
|
310
|
+
urls: options.urls,
|
|
311
|
+
timezone: options.timezone,
|
|
312
|
+
currency: options.currency,
|
|
313
|
+
...options.raw ?? {}
|
|
314
|
+
});
|
|
315
|
+
const site = await this.getSite(options.siteId);
|
|
316
|
+
if (!site) {
|
|
317
|
+
throw new AnalyticsError(
|
|
318
|
+
`Matomo site ${options.siteId} was not found after update`,
|
|
319
|
+
"MATOMO_SITE_NOT_FOUND",
|
|
320
|
+
PROVIDER$1
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return { ...site, tenantId: options.tenantId };
|
|
324
|
+
}
|
|
325
|
+
async deleteSite(siteId) {
|
|
326
|
+
await this.transport.call("SitesManager.deleteSite", {
|
|
327
|
+
idSite: siteId
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Users
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
async createUser(options) {
|
|
334
|
+
if (!options.password) {
|
|
335
|
+
throw new AnalyticsError(
|
|
336
|
+
"Matomo createUser requires a password",
|
|
337
|
+
"MATOMO_PASSWORD_REQUIRED",
|
|
338
|
+
PROVIDER$1
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
await this.transport.call("UsersManager.addUser", {
|
|
342
|
+
userLogin: options.login,
|
|
343
|
+
password: options.password,
|
|
344
|
+
email: options.email,
|
|
345
|
+
...options.raw ?? {}
|
|
346
|
+
});
|
|
347
|
+
const user = await this.getUser(options.login);
|
|
348
|
+
return user ?? {
|
|
349
|
+
login: options.login,
|
|
350
|
+
email: options.email,
|
|
351
|
+
tenantId: options.tenantId,
|
|
352
|
+
provider: PROVIDER$1
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async getUser(login) {
|
|
356
|
+
try {
|
|
357
|
+
const response = await this.transport.call(
|
|
358
|
+
"UsersManager.getUser",
|
|
359
|
+
{ userLogin: login }
|
|
360
|
+
);
|
|
361
|
+
if (isJsonRecord(response)) return userFromRow(response);
|
|
362
|
+
if (Array.isArray(response) && response.length === 0) return void 0;
|
|
363
|
+
return void 0;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (error instanceof AnalyticsError && error.code === "MATOMO_API_ERROR" && /doesn'?t exist|does not exist|unknown/i.test(error.message)) {
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async deleteUser(login) {
|
|
372
|
+
await this.transport.call("UsersManager.deleteUser", {
|
|
373
|
+
userLogin: login
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
async setUserAccess(options) {
|
|
377
|
+
if (!isAccessRole(options.access)) {
|
|
378
|
+
throw new AnalyticsError(
|
|
379
|
+
`Invalid Matomo access role: ${options.access}`,
|
|
380
|
+
"MATOMO_INVALID_ROLE",
|
|
381
|
+
PROVIDER$1
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (!options.siteIds || options.siteIds.length === 0) {
|
|
385
|
+
throw new AnalyticsError(
|
|
386
|
+
"Matomo setUserAccess requires at least one site id",
|
|
387
|
+
"MATOMO_SITE_IDS_REQUIRED",
|
|
388
|
+
PROVIDER$1
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
await this.transport.call("UsersManager.setUserAccess", {
|
|
392
|
+
userLogin: options.login,
|
|
393
|
+
access: options.access,
|
|
394
|
+
idSites: options.siteIds
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async verifyUserSiteAccess(options) {
|
|
398
|
+
const requiredAccess = options.minimumAccess ?? "view";
|
|
399
|
+
if (!isAccessRole(requiredAccess)) {
|
|
400
|
+
throw new AnalyticsError(
|
|
401
|
+
`Invalid Matomo access role: ${requiredAccess}`,
|
|
402
|
+
"MATOMO_INVALID_ROLE",
|
|
403
|
+
PROVIDER$1
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const response = await this.transport.call(
|
|
408
|
+
"UsersManager.getSitesAccessFromUser",
|
|
409
|
+
{ userLogin: options.login }
|
|
410
|
+
);
|
|
411
|
+
const entries = normalizeSiteAccessEntries(response);
|
|
412
|
+
const match = entries.find((entry) => entry.siteId === options.siteId);
|
|
413
|
+
let access = match?.access ?? "noaccess";
|
|
414
|
+
if (access === "noaccess") {
|
|
415
|
+
const user = await this.getUser(options.login);
|
|
416
|
+
if (user?.isSuperUser) {
|
|
417
|
+
access = "admin";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const ok = hasMinimumAccess(access, requiredAccess);
|
|
421
|
+
return {
|
|
422
|
+
ok,
|
|
423
|
+
provider: PROVIDER$1,
|
|
424
|
+
login: options.login,
|
|
425
|
+
siteId: options.siteId,
|
|
426
|
+
access,
|
|
427
|
+
requiredAccess,
|
|
428
|
+
errorCode: ok ? void 0 : "MATOMO_ACCESS_DENIED",
|
|
429
|
+
error: ok ? void 0 : `User "${options.login}" has ${access} access to site ${options.siteId}; ${requiredAccess} is required.`,
|
|
430
|
+
raw: response
|
|
431
|
+
};
|
|
432
|
+
} catch (error) {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
provider: PROVIDER$1,
|
|
436
|
+
login: options.login,
|
|
437
|
+
siteId: options.siteId,
|
|
438
|
+
access: "noaccess",
|
|
439
|
+
requiredAccess,
|
|
440
|
+
error: error instanceof Error ? error.message : String(error),
|
|
441
|
+
errorCode: error instanceof AnalyticsError ? error.code : void 0
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async verifyTokenSiteAccess(options) {
|
|
446
|
+
try {
|
|
447
|
+
const transport = this.cloneTransportWithToken(options.tokenAuth);
|
|
448
|
+
const response = await transport.call(
|
|
449
|
+
"SitesManager.getSiteFromId",
|
|
450
|
+
{ idSite: options.siteId }
|
|
451
|
+
);
|
|
452
|
+
if (Array.isArray(response) && response.length === 0) {
|
|
453
|
+
return {
|
|
454
|
+
ok: false,
|
|
455
|
+
provider: PROVIDER$1,
|
|
456
|
+
siteId: options.siteId,
|
|
457
|
+
access: "noaccess",
|
|
458
|
+
requiredAccess: "view",
|
|
459
|
+
errorCode: "MATOMO_SITE_NOT_FOUND",
|
|
460
|
+
error: `Site ${options.siteId} was not found.`,
|
|
461
|
+
raw: response
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const siteVisible = isJsonRecord(response);
|
|
465
|
+
if (siteVisible) {
|
|
466
|
+
siteFromRow(response);
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
ok: siteVisible,
|
|
470
|
+
provider: PROVIDER$1,
|
|
471
|
+
siteId: options.siteId,
|
|
472
|
+
access: siteVisible ? "view" : "noaccess",
|
|
473
|
+
requiredAccess: "view",
|
|
474
|
+
errorCode: siteVisible ? void 0 : "MATOMO_ACCESS_DENIED",
|
|
475
|
+
error: siteVisible ? void 0 : `Token cannot read site ${options.siteId}.`,
|
|
476
|
+
raw: response
|
|
477
|
+
};
|
|
478
|
+
} catch (error) {
|
|
479
|
+
const errorCode = error instanceof AnalyticsError ? error.code : void 0;
|
|
480
|
+
return {
|
|
481
|
+
ok: false,
|
|
482
|
+
provider: PROVIDER$1,
|
|
483
|
+
siteId: options.siteId,
|
|
484
|
+
access: "noaccess",
|
|
485
|
+
requiredAccess: "view",
|
|
486
|
+
error: errorCode === "MATOMO_ACCESS_DENIED" ? `Token cannot read site ${options.siteId} (no view grant).` : error instanceof Error ? error.message : String(error),
|
|
487
|
+
errorCode
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async mintUserToken(options) {
|
|
492
|
+
if (!options.passwordConfirmation) {
|
|
493
|
+
throw new AnalyticsError(
|
|
494
|
+
"Matomo's createAppSpecificTokenAuth requires the target user's password as `passwordConfirmation`. (It confirms the user being minted for — not the caller — so a stolen super-user token alone can't mint tokens for arbitrary users.)",
|
|
495
|
+
"MATOMO_PASSWORD_CONFIRMATION_REQUIRED",
|
|
496
|
+
PROVIDER$1
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const effectiveDescription = options.description ?? `analytics-token ${options.login}`;
|
|
500
|
+
const response = await this.transport.call(
|
|
501
|
+
"UsersManager.createAppSpecificTokenAuth",
|
|
502
|
+
{
|
|
503
|
+
userLogin: options.login,
|
|
504
|
+
passwordConfirmation: options.passwordConfirmation,
|
|
505
|
+
description: effectiveDescription,
|
|
506
|
+
expireHours: 0
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
const token = isJsonRecord(response) && readString$1(response, "value") || (typeof response === "string" ? response : void 0);
|
|
510
|
+
if (!token) {
|
|
511
|
+
throw new AnalyticsError(
|
|
512
|
+
"Matomo did not return a token value",
|
|
513
|
+
"MATOMO_INVALID_RESPONSE",
|
|
514
|
+
PROVIDER$1
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
token,
|
|
519
|
+
login: options.login,
|
|
520
|
+
description: effectiveDescription,
|
|
521
|
+
provider: PROVIDER$1,
|
|
522
|
+
raw: response
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
// Health
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
async health() {
|
|
529
|
+
try {
|
|
530
|
+
const response = await this.transport.call(
|
|
531
|
+
"API.getMatomoVersion",
|
|
532
|
+
{}
|
|
533
|
+
);
|
|
534
|
+
const version = isJsonRecord(response) && readString$1(response, "value") || (typeof response === "string" ? response : void 0);
|
|
535
|
+
return { ok: true, version };
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return {
|
|
538
|
+
ok: false,
|
|
539
|
+
error: error instanceof Error ? error.message : String(error)
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function coerceSiteIdResponse(response) {
|
|
545
|
+
if (typeof response === "number" && Number.isFinite(response)) {
|
|
546
|
+
return String(response);
|
|
547
|
+
}
|
|
548
|
+
if (typeof response === "string" && response.length > 0) {
|
|
549
|
+
return response;
|
|
550
|
+
}
|
|
551
|
+
if (isJsonRecord(response)) {
|
|
552
|
+
return readString$1(response, "value") ?? readString$1(response, "idsite") ?? readString$1(response, "idSite");
|
|
553
|
+
}
|
|
554
|
+
return void 0;
|
|
555
|
+
}
|
|
556
|
+
function isAccessRole(value) {
|
|
557
|
+
return value === "noaccess" || value === "view" || value === "write" || value === "admin";
|
|
558
|
+
}
|
|
559
|
+
function normalizeSiteAccessEntries(response) {
|
|
560
|
+
if (Array.isArray(response)) {
|
|
561
|
+
return response.filter(isJsonRecord).map((row) => {
|
|
562
|
+
const siteId = readString$1(row, "site") ?? readString$1(row, "idsite") ?? readString$1(row, "idSite");
|
|
563
|
+
const access = readString$1(row, "access");
|
|
564
|
+
return siteId && access && isAccessRole(access) ? { siteId, access } : void 0;
|
|
565
|
+
}).filter((entry) => !!entry);
|
|
566
|
+
}
|
|
567
|
+
if (isJsonRecord(response)) {
|
|
568
|
+
const entries = [];
|
|
569
|
+
for (const [siteId, access] of Object.entries(response)) {
|
|
570
|
+
if (typeof access === "string" && isAccessRole(access)) {
|
|
571
|
+
entries.push({ siteId, access });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return entries;
|
|
575
|
+
}
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
const ACCESS_RANK = {
|
|
579
|
+
noaccess: 0,
|
|
580
|
+
view: 1,
|
|
581
|
+
write: 2,
|
|
582
|
+
admin: 3
|
|
583
|
+
};
|
|
584
|
+
function hasMinimumAccess(access, minimumAccess) {
|
|
585
|
+
return ACCESS_RANK[access] >= ACCESS_RANK[minimumAccess];
|
|
586
|
+
}
|
|
587
|
+
function isNotFoundSiteError(error) {
|
|
588
|
+
if (error.code !== "MATOMO_API_ERROR") return false;
|
|
589
|
+
return /not.*found|doesn'?t exist|does not exist|unknown/i.test(error.message) || /website was found in the request|website id was set/i.test(error.message);
|
|
590
|
+
}
|
|
591
|
+
const PROVIDER = "matomo";
|
|
592
|
+
class MatomoProvider {
|
|
593
|
+
baseUrl;
|
|
594
|
+
reportingTransport;
|
|
595
|
+
admin;
|
|
596
|
+
constructor(options) {
|
|
597
|
+
this.baseUrl = normalizeMatomoBaseUrl(options.baseUrl);
|
|
598
|
+
this.reportingTransport = new MatomoAdminTransport({
|
|
599
|
+
baseUrl: this.baseUrl,
|
|
600
|
+
tokenAuth: options.tokenAuth,
|
|
601
|
+
timeout: options.timeout
|
|
602
|
+
});
|
|
603
|
+
this.admin = new MatomoAdmin({
|
|
604
|
+
baseUrl: this.baseUrl,
|
|
605
|
+
tokenAuth: options.tokenAuth,
|
|
606
|
+
timeout: options.timeout
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
// Property management — delegated to admin (site === property in Matomo)
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
createProperty(_options) {
|
|
613
|
+
throw new NotSupportedError(
|
|
614
|
+
"createProperty (use admin.createSite instead)",
|
|
615
|
+
PROVIDER
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
async listProperties(_options) {
|
|
619
|
+
const sites = await this.admin.listSites();
|
|
620
|
+
return sites.map((site) => propertyFromSite(site));
|
|
621
|
+
}
|
|
622
|
+
async getProperty(propertyId) {
|
|
623
|
+
const site = await this.admin.getSite(propertyId);
|
|
624
|
+
if (!site) {
|
|
625
|
+
throw new PropertyNotFoundError(propertyId, PROVIDER);
|
|
626
|
+
}
|
|
627
|
+
return propertyFromSite(site);
|
|
628
|
+
}
|
|
629
|
+
async updateProperty(_propertyId, _data) {
|
|
630
|
+
throw new NotSupportedError("updateProperty", PROVIDER);
|
|
631
|
+
}
|
|
632
|
+
async deleteProperty(propertyId) {
|
|
633
|
+
await this.admin.deleteSite(propertyId);
|
|
634
|
+
}
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
// Capabilities
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
async getCapabilities() {
|
|
639
|
+
return {
|
|
640
|
+
propertyManagement: true,
|
|
641
|
+
dataStreams: false,
|
|
642
|
+
customDimensions: false,
|
|
643
|
+
customMetrics: false,
|
|
644
|
+
keyEvents: false,
|
|
645
|
+
reporting: true,
|
|
646
|
+
realtimeReporting: true,
|
|
647
|
+
serverSideTracking: false,
|
|
648
|
+
clientSideSnippet: true,
|
|
649
|
+
userIdentification: false,
|
|
650
|
+
batchTracking: false
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
// ---------------------------------------------------------------------------
|
|
654
|
+
// Client-side snippet
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
/**
|
|
657
|
+
* Generate a Matomo `_paq` tracking snippet.
|
|
658
|
+
*
|
|
659
|
+
* Note on `SnippetOptions.anonymizeIp`: Matomo handles IP anonymization
|
|
660
|
+
* **server-side** via the PrivacyManager plugin (Matomo admin → Privacy →
|
|
661
|
+
* Anonymize visitors' IP addresses). The JS tracker has no equivalent
|
|
662
|
+
* client-side directive — `setDoNotTrack` respects the browser's DNT
|
|
663
|
+
* signal but does not anonymize IPs, so we deliberately do not emit it
|
|
664
|
+
* here. The flag is preserved on the returned `config` object as a
|
|
665
|
+
* caller-visible signal, but its enforcement is the operator's
|
|
666
|
+
* responsibility on the Matomo install.
|
|
667
|
+
*/
|
|
668
|
+
generateTrackingSnippet(propertyId, options = {}) {
|
|
669
|
+
const trackerUrl = `${this.baseUrl}/matomo.php`;
|
|
670
|
+
const scriptUrl = `${this.baseUrl}/matomo.js`;
|
|
671
|
+
const sendPageView = options.sendPageView ?? true;
|
|
672
|
+
const lines = ["var _paq = window._paq = window._paq || [];"];
|
|
673
|
+
if (sendPageView) {
|
|
674
|
+
lines.push("_paq.push(['trackPageView']);");
|
|
675
|
+
}
|
|
676
|
+
lines.push("_paq.push(['enableLinkTracking']);");
|
|
677
|
+
if (options.customConfig) {
|
|
678
|
+
for (const [key, value] of Object.entries(options.customConfig)) {
|
|
679
|
+
lines.push(
|
|
680
|
+
`_paq.push([${JSON.stringify(key)}, ${JSON.stringify(value)}]);`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
lines.push(
|
|
685
|
+
`(function() { var u=${JSON.stringify(`${this.baseUrl}/`)}; _paq.push(['setTrackerUrl', u+'matomo.php']); _paq.push(['setSiteId', ${JSON.stringify(propertyId)}]); var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); })();`
|
|
686
|
+
);
|
|
687
|
+
return {
|
|
688
|
+
html: `<script>
|
|
689
|
+
${lines.join("\n")}
|
|
690
|
+
<\/script>`,
|
|
691
|
+
config: {
|
|
692
|
+
trackerUrl,
|
|
693
|
+
scriptUrl,
|
|
694
|
+
siteId: propertyId,
|
|
695
|
+
anonymizeIp: !!options.anonymizeIp,
|
|
696
|
+
sendPageView
|
|
697
|
+
},
|
|
698
|
+
scripts: [scriptUrl]
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
generateConfig(propertyId, options = {}) {
|
|
702
|
+
return {
|
|
703
|
+
trackerUrl: `${this.baseUrl}/matomo.php`,
|
|
704
|
+
scriptUrl: `${this.baseUrl}/matomo.js`,
|
|
705
|
+
siteId: propertyId,
|
|
706
|
+
anonymizeIp: !!options.anonymizeIp,
|
|
707
|
+
sendPageView: options.sendPageView ?? true,
|
|
708
|
+
userId: options.userId,
|
|
709
|
+
customDimensions: options.customDimensions
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
// ---------------------------------------------------------------------------
|
|
713
|
+
// Methods deferred to a follow-up PR — they have meaningful Matomo analogues
|
|
714
|
+
// but warrant their own tests and a wider review surface.
|
|
715
|
+
// ---------------------------------------------------------------------------
|
|
716
|
+
getDataStreams(_propertyId) {
|
|
717
|
+
throw new NotSupportedError("dataStreams", PROVIDER);
|
|
718
|
+
}
|
|
719
|
+
createDataStream(_propertyId, _options) {
|
|
720
|
+
throw new NotSupportedError("dataStreams", PROVIDER);
|
|
721
|
+
}
|
|
722
|
+
deleteDataStream(_propertyId, _streamId) {
|
|
723
|
+
throw new NotSupportedError("dataStreams", PROVIDER);
|
|
724
|
+
}
|
|
725
|
+
getCustomDimensions(_propertyId) {
|
|
726
|
+
throw new NotSupportedError("customDimensions", PROVIDER);
|
|
727
|
+
}
|
|
728
|
+
createCustomDimension(_propertyId, _options) {
|
|
729
|
+
throw new NotSupportedError("customDimensions", PROVIDER);
|
|
730
|
+
}
|
|
731
|
+
archiveCustomDimension(_propertyId, _dimensionId) {
|
|
732
|
+
throw new NotSupportedError("customDimensions", PROVIDER);
|
|
733
|
+
}
|
|
734
|
+
getCustomMetrics(_propertyId) {
|
|
735
|
+
throw new NotSupportedError("customMetrics", PROVIDER);
|
|
736
|
+
}
|
|
737
|
+
createCustomMetric(_propertyId, _options) {
|
|
738
|
+
throw new NotSupportedError("customMetrics", PROVIDER);
|
|
739
|
+
}
|
|
740
|
+
archiveCustomMetric(_propertyId, _metricId) {
|
|
741
|
+
throw new NotSupportedError("customMetrics", PROVIDER);
|
|
742
|
+
}
|
|
743
|
+
getKeyEvents(_propertyId) {
|
|
744
|
+
throw new NotSupportedError("keyEvents", PROVIDER);
|
|
745
|
+
}
|
|
746
|
+
createKeyEvent(_propertyId, _options) {
|
|
747
|
+
throw new NotSupportedError("keyEvents", PROVIDER);
|
|
748
|
+
}
|
|
749
|
+
deleteKeyEvent(_propertyId, _eventId) {
|
|
750
|
+
throw new NotSupportedError("keyEvents", PROVIDER);
|
|
751
|
+
}
|
|
752
|
+
async runReport(propertyId, options) {
|
|
753
|
+
const dimensions = options.dimensions ?? [];
|
|
754
|
+
const metrics = options.metrics;
|
|
755
|
+
const dateRange = options.dateRanges[0] ?? {
|
|
756
|
+
startDate: "7daysAgo",
|
|
757
|
+
endDate: "today"
|
|
758
|
+
};
|
|
759
|
+
const query = matomoDateQuery(dateRange);
|
|
760
|
+
const dimensionNames = dimensions.map((dimension) => dimension.name);
|
|
761
|
+
const method = reportMethodForDimensions(dimensionNames);
|
|
762
|
+
const response = await this.reportingTransport.call(method, {
|
|
763
|
+
idSite: propertyId,
|
|
764
|
+
period: method === "VisitsSummary.get" ? "day" : query.period,
|
|
765
|
+
date: query.date,
|
|
766
|
+
filter_limit: options.limit,
|
|
767
|
+
filter_offset: options.offset,
|
|
768
|
+
flat: dimensionNames.some(isPageDimension) ? 1 : void 0
|
|
769
|
+
});
|
|
770
|
+
const sourceRows = normalizeMatomoRows(response);
|
|
771
|
+
const rows = sourceRows.map((row) => ({
|
|
772
|
+
dimensionValues: dimensions.map((dimension) => ({
|
|
773
|
+
value: dimensionValue(row, dimension.name)
|
|
774
|
+
})),
|
|
775
|
+
metricValues: metrics.map((metric) => ({
|
|
776
|
+
value: metricValue(row, metric.name)
|
|
777
|
+
}))
|
|
778
|
+
}));
|
|
779
|
+
return {
|
|
780
|
+
dimensionHeaders: dimensions.map((dimension) => ({
|
|
781
|
+
name: dimension.name
|
|
782
|
+
})),
|
|
783
|
+
metricHeaders: metrics.map((metric) => ({
|
|
784
|
+
name: metric.name,
|
|
785
|
+
type: metricType(metric.name)
|
|
786
|
+
})),
|
|
787
|
+
rows,
|
|
788
|
+
rowCount: rows.length
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
async runRealtimeReport(propertyId, options = {}) {
|
|
792
|
+
const dimensions = options.dimensions ?? [];
|
|
793
|
+
const metrics = options.metrics ?? [{ name: "activeUsers" }];
|
|
794
|
+
const limit = options.limit ?? 10;
|
|
795
|
+
const lastMinutes = options.minuteRanges?.[0]?.startMinutesAgo ?? 30;
|
|
796
|
+
if (dimensions.some(isPageDimension)) {
|
|
797
|
+
const response2 = await this.reportingTransport.call(
|
|
798
|
+
"Live.getLastVisitsDetails",
|
|
799
|
+
{
|
|
800
|
+
idSite: propertyId,
|
|
801
|
+
period: "day",
|
|
802
|
+
date: "today",
|
|
803
|
+
filter_limit: limit,
|
|
804
|
+
lastMinutes
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
const rows = realtimePageRows(response2, dimensions, metrics, limit);
|
|
808
|
+
return {
|
|
809
|
+
dimensionHeaders: dimensions.map((dimension) => ({
|
|
810
|
+
name: dimension.name
|
|
811
|
+
})),
|
|
812
|
+
metricHeaders: metrics.map((metric) => ({
|
|
813
|
+
name: metric.name,
|
|
814
|
+
type: metricType(metric.name)
|
|
815
|
+
})),
|
|
816
|
+
rows,
|
|
817
|
+
rowCount: rows.length
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const response = await this.reportingTransport.call(
|
|
821
|
+
"Live.getCounters",
|
|
822
|
+
{
|
|
823
|
+
idSite: propertyId,
|
|
824
|
+
lastMinutes
|
|
825
|
+
}
|
|
826
|
+
);
|
|
827
|
+
const counters = firstRecord(response);
|
|
828
|
+
const row = {
|
|
829
|
+
dimensionValues: dimensions.map((dimension) => ({
|
|
830
|
+
value: dimensionValue(counters, dimension.name)
|
|
831
|
+
})),
|
|
832
|
+
metricValues: metrics.map((metric) => ({
|
|
833
|
+
value: realtimeMetricValue(counters, metric.name)
|
|
834
|
+
}))
|
|
835
|
+
};
|
|
836
|
+
return {
|
|
837
|
+
dimensionHeaders: dimensions.map((dimension) => ({
|
|
838
|
+
name: dimension.name
|
|
839
|
+
})),
|
|
840
|
+
metricHeaders: metrics.map((metric) => ({
|
|
841
|
+
name: metric.name,
|
|
842
|
+
type: metricType(metric.name)
|
|
843
|
+
})),
|
|
844
|
+
rows: [row],
|
|
845
|
+
rowCount: 1
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
getMetrics(_propertyId) {
|
|
849
|
+
throw new NotSupportedError("getMetrics", PROVIDER);
|
|
850
|
+
}
|
|
851
|
+
getDimensions(_propertyId) {
|
|
852
|
+
throw new NotSupportedError("getDimensions", PROVIDER);
|
|
853
|
+
}
|
|
854
|
+
track(_event) {
|
|
855
|
+
throw new NotSupportedError("track", PROVIDER);
|
|
856
|
+
}
|
|
857
|
+
trackPageview(_pageview) {
|
|
858
|
+
throw new NotSupportedError("trackPageview", PROVIDER);
|
|
859
|
+
}
|
|
860
|
+
trackBatch(_events) {
|
|
861
|
+
throw new NotSupportedError("trackBatch", PROVIDER);
|
|
862
|
+
}
|
|
863
|
+
identify(_userId, _traits) {
|
|
864
|
+
throw new NotSupportedError("identify", PROVIDER);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function propertyFromSite(site) {
|
|
868
|
+
return {
|
|
869
|
+
id: site.id,
|
|
870
|
+
name: `sites/${site.id}`,
|
|
871
|
+
displayName: site.name,
|
|
872
|
+
createTime: "",
|
|
873
|
+
timeZone: site.timezone,
|
|
874
|
+
currencyCode: site.currency
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function isRecord(value) {
|
|
878
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
879
|
+
}
|
|
880
|
+
function firstRecord(value) {
|
|
881
|
+
if (Array.isArray(value)) {
|
|
882
|
+
return value.find(isRecord) ?? {};
|
|
883
|
+
}
|
|
884
|
+
return isRecord(value) ? value : {};
|
|
885
|
+
}
|
|
886
|
+
function normalizeMatomoRows(value) {
|
|
887
|
+
if (Array.isArray(value)) {
|
|
888
|
+
return value.filter(isRecord);
|
|
889
|
+
}
|
|
890
|
+
if (!isRecord(value)) {
|
|
891
|
+
return [];
|
|
892
|
+
}
|
|
893
|
+
const entries = Object.entries(value);
|
|
894
|
+
if (entries.length > 0 && entries.every(([, row]) => isRecord(row))) {
|
|
895
|
+
return entries.map(([key, row]) => ({ ...row, date: key }));
|
|
896
|
+
}
|
|
897
|
+
return [value];
|
|
898
|
+
}
|
|
899
|
+
function matomoDateQuery(range) {
|
|
900
|
+
if (range.endDate === "today") {
|
|
901
|
+
const daysAgo = /^(\d+)daysAgo$/.exec(range.startDate);
|
|
902
|
+
if (daysAgo) {
|
|
903
|
+
return { period: "range", date: `last${daysAgo[1]}` };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (range.startDate === range.endDate) {
|
|
907
|
+
return { period: "day", date: range.startDate };
|
|
908
|
+
}
|
|
909
|
+
return { period: "range", date: `${range.startDate},${range.endDate}` };
|
|
910
|
+
}
|
|
911
|
+
function reportMethodForDimensions(dimensions) {
|
|
912
|
+
if (dimensions.length === 0) {
|
|
913
|
+
return "VisitsSummary.get";
|
|
914
|
+
}
|
|
915
|
+
if (dimensions.every((dimension) => dimension === "date")) {
|
|
916
|
+
return "VisitsSummary.get";
|
|
917
|
+
}
|
|
918
|
+
if (dimensions.every(isPageDimension)) {
|
|
919
|
+
return "Actions.getPageUrls";
|
|
920
|
+
}
|
|
921
|
+
if (dimensions.every(
|
|
922
|
+
(dimension) => dimension === "sessionSource" || dimension === "sessionMedium"
|
|
923
|
+
)) {
|
|
924
|
+
return "Referrers.getAll";
|
|
925
|
+
}
|
|
926
|
+
throw new NotSupportedError(
|
|
927
|
+
`Matomo report dimensions (${dimensions.join(", ")})`,
|
|
928
|
+
PROVIDER
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
function isPageDimension(dimension) {
|
|
932
|
+
const name = typeof dimension === "string" ? dimension : dimension.name;
|
|
933
|
+
return [
|
|
934
|
+
"pagePath",
|
|
935
|
+
"pageTitle",
|
|
936
|
+
"unifiedScreenName",
|
|
937
|
+
"unifiedPagePathScreen"
|
|
938
|
+
].includes(name);
|
|
939
|
+
}
|
|
940
|
+
function dimensionValue(row, name) {
|
|
941
|
+
switch (name) {
|
|
942
|
+
case "date":
|
|
943
|
+
return readString(row.date) ?? "";
|
|
944
|
+
case "pagePath":
|
|
945
|
+
case "unifiedPagePathScreen":
|
|
946
|
+
return readString(row.url) ?? readString(row.label) ?? "";
|
|
947
|
+
case "pageTitle":
|
|
948
|
+
case "unifiedScreenName":
|
|
949
|
+
return readString(row.pageTitle) ?? readString(row.title) ?? readString(row.label) ?? "";
|
|
950
|
+
case "sessionSource":
|
|
951
|
+
return readString(row.label) ?? readString(row.referer_name) ?? readString(row.refererName) ?? "";
|
|
952
|
+
case "sessionMedium":
|
|
953
|
+
return readString(row.referer_type) ?? readString(row.refererType) ?? readString(row.type) ?? "";
|
|
954
|
+
default:
|
|
955
|
+
return readString(row[name]) ?? "";
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function metricValue(row, name) {
|
|
959
|
+
switch (name) {
|
|
960
|
+
case "activeUsers":
|
|
961
|
+
return readNumericString(row.nb_uniq_visitors ?? row.nb_users);
|
|
962
|
+
case "sessions":
|
|
963
|
+
return readNumericString(row.nb_visits);
|
|
964
|
+
case "bounceRate":
|
|
965
|
+
return readNumericString(row.bounce_rate);
|
|
966
|
+
case "averageSessionDuration":
|
|
967
|
+
return readNumericString(row.avg_time_on_site);
|
|
968
|
+
case "screenPageViews":
|
|
969
|
+
return readNumericString(row.nb_hits ?? row.nb_pageviews);
|
|
970
|
+
default:
|
|
971
|
+
return readNumericString(row[name]);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function realtimeMetricValue(row, name) {
|
|
975
|
+
switch (name) {
|
|
976
|
+
case "activeUsers":
|
|
977
|
+
return readNumericString(row.visitors ?? row.nb_uniq_visitors);
|
|
978
|
+
case "sessions":
|
|
979
|
+
return readNumericString(row.visits ?? row.nb_visits);
|
|
980
|
+
case "screenPageViews":
|
|
981
|
+
return readNumericString(row.actions ?? row.nb_hits ?? row.nb_pageviews);
|
|
982
|
+
default:
|
|
983
|
+
return metricValue(row, name);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function metricType(name) {
|
|
987
|
+
return name === "bounceRate" ? "TYPE_FLOAT" : "TYPE_INTEGER";
|
|
988
|
+
}
|
|
989
|
+
function readString(value) {
|
|
990
|
+
if (typeof value === "string") return value;
|
|
991
|
+
if (typeof value === "number") return String(value);
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
function readNumericString(value) {
|
|
995
|
+
if (typeof value === "number") return String(value);
|
|
996
|
+
if (typeof value !== "string") return "0";
|
|
997
|
+
const trimmed = value.trim();
|
|
998
|
+
if (!trimmed) return "0";
|
|
999
|
+
if (trimmed.includes(":")) {
|
|
1000
|
+
return String(parseDurationSeconds(trimmed));
|
|
1001
|
+
}
|
|
1002
|
+
const numeric = Number(trimmed.replace("%", ""));
|
|
1003
|
+
return Number.isFinite(numeric) ? String(numeric) : "0";
|
|
1004
|
+
}
|
|
1005
|
+
function parseDurationSeconds(value) {
|
|
1006
|
+
const parts = value.split(":").map((part) => Number(part));
|
|
1007
|
+
if (parts.some((part) => !Number.isFinite(part))) return 0;
|
|
1008
|
+
return parts.reduce((total, part) => total * 60 + part, 0);
|
|
1009
|
+
}
|
|
1010
|
+
function realtimePageRows(response, dimensions, metrics, limit) {
|
|
1011
|
+
const pageCounts = /* @__PURE__ */ new Map();
|
|
1012
|
+
const visits = normalizeMatomoRows(response);
|
|
1013
|
+
for (const visit of visits) {
|
|
1014
|
+
const visitorId = readString(visit.visitorId) ?? readString(visit.idVisit) ?? "";
|
|
1015
|
+
const actions = Array.isArray(visit.actionDetails) ? visit.actionDetails.filter(isRecord) : [];
|
|
1016
|
+
for (const action of actions) {
|
|
1017
|
+
const title = readString(action.pageTitle) ?? readString(action.title) ?? readString(action.url) ?? "";
|
|
1018
|
+
const url = readString(action.url) ?? title;
|
|
1019
|
+
const key = `${title}\0${url}`;
|
|
1020
|
+
const existing = pageCounts.get(key) ?? {
|
|
1021
|
+
pageTitle: title,
|
|
1022
|
+
url,
|
|
1023
|
+
visitors: /* @__PURE__ */ new Set(),
|
|
1024
|
+
actions: 0
|
|
1025
|
+
};
|
|
1026
|
+
existing.actions = Number(existing.actions ?? 0) + 1;
|
|
1027
|
+
if (visitorId) existing.visitors.add(visitorId);
|
|
1028
|
+
pageCounts.set(key, existing);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return [...pageCounts.values()].sort((a, b) => Number(b.actions ?? 0) - Number(a.actions ?? 0)).slice(0, limit).map((row) => ({
|
|
1032
|
+
dimensionValues: dimensions.map((dimension) => ({
|
|
1033
|
+
value: dimensionValue(row, dimension.name)
|
|
1034
|
+
})),
|
|
1035
|
+
metricValues: metrics.map((metric) => ({
|
|
1036
|
+
value: metric.name === "activeUsers" ? String(row.visitors.size) : realtimeMetricValue(row, metric.name)
|
|
1037
|
+
}))
|
|
1038
|
+
}));
|
|
1039
|
+
}
|
|
1040
|
+
export {
|
|
1041
|
+
MatomoProvider
|
|
1042
|
+
};
|
|
1043
|
+
//# sourceMappingURL=matomo-Ds_oRmZ6.js.map
|