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