@nodewrite-labs/analytics-sdk 0.1.0 → 0.3.0
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/LICENSE +21 -21
- package/README.md +21 -0
- package/dist/chunk-MOCOVYQK.js +108 -0
- package/dist/{chunk-GVOESWHZ.js → chunk-QPHMFHRE.js} +103 -47
- package/dist/{chunk-Q4JRXZ6M.js → chunk-YAC3MRH3.js} +14 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +3 -1
- package/dist/{event-store-BhisLdw_.d.ts → event-store-BkSuP4ZE.d.ts} +42 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +13 -1
- package/dist/options-Dpx2vs4R.d.ts +29 -0
- package/dist/payload/admin-filters.d.ts +24 -0
- package/dist/payload/admin-filters.js +33 -0
- package/dist/payload/admin-view.d.ts +2 -1
- package/dist/payload/admin-view.js +683 -250
- package/dist/payload/index.d.ts +74 -39
- package/dist/payload/index.js +44 -13
- package/dist/payload/nav-link.d.ts +6 -1
- package/dist/payload/nav-link.js +25 -3
- package/dist/postgres/index.js +1 -1
- package/dist/server/index.d.ts +4 -7
- package/package.json +26 -3
- package/src/postgres/schema.sql +12 -12
- package/dist/options-D3s0dcJ-.d.ts +0 -19
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 nodewrite-hq
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nodewrite-hq
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -75,6 +75,27 @@ const result = await recordAnalyticsEvent(
|
|
|
75
75
|
|
|
76
76
|
`enabled: false` writes nothing and returns status 204. A stored view or click returns status 200 and `{ id }`. A recorded duration returns 204. An event that cannot be stored returns 400.
|
|
77
77
|
|
|
78
|
+
## Payload
|
|
79
|
+
|
|
80
|
+
`analyticsPlugin` adds the hidden `analytics-events` collection, the ingest endpoint at `/analytics/events` under the Payload API route (`/api/analytics/events` by default), and an admin screen at `/analytics` under the admin route. The nav link follows `routes.admin`.
|
|
81
|
+
|
|
82
|
+
The package names no host's geo headers. Pass `geo` to read country and city from the request headers your host sets. Without `geo`, the country and city modules store null.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { analyticsPlugin } from "@nodewrite-labs/analytics-sdk/payload";
|
|
86
|
+
|
|
87
|
+
analyticsPlugin({
|
|
88
|
+
enabled: true,
|
|
89
|
+
modules: { country: true, city: false, sessions: false, clicks: true },
|
|
90
|
+
// Vercel, for example. Cloudflare sends `cf-ipcountry`.
|
|
91
|
+
geo: (headers) => ({ country: headers.get("x-vercel-ip-country"), city: null }),
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The browser then posts to the Payload endpoint: `startAnalytics({ endpoint: "/api/analytics/events", … })`.
|
|
96
|
+
|
|
97
|
+
The plugin adds a collection, so a Payload project with migrations needs a new one (`payload migrate:create`).
|
|
98
|
+
|
|
78
99
|
## Store
|
|
79
100
|
|
|
80
101
|
`AnalyticsEventStore` is three methods: `insertAnalyticsEvent`, `recordAnalyticsDuration`, and `summarizeAnalytics`.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/core/period.ts
|
|
2
|
+
var AnalyticsPeriod = {
|
|
3
|
+
LAST_24_HOURS: "24h",
|
|
4
|
+
LAST_7_DAYS: "7d",
|
|
5
|
+
LAST_30_DAYS: "30d",
|
|
6
|
+
LAST_12_MONTHS: "12m"
|
|
7
|
+
};
|
|
8
|
+
var ANALYTICS_PERIODS = Object.values(AnalyticsPeriod);
|
|
9
|
+
var DEFAULT_ANALYTICS_PERIOD = AnalyticsPeriod.LAST_7_DAYS;
|
|
10
|
+
var AnalyticsBucketUnit = {
|
|
11
|
+
HOUR: "hour",
|
|
12
|
+
DAY: "day",
|
|
13
|
+
MONTH: "month"
|
|
14
|
+
};
|
|
15
|
+
var PERIOD_BUCKETS = {
|
|
16
|
+
[AnalyticsPeriod.LAST_24_HOURS]: { unit: AnalyticsBucketUnit.HOUR, count: 24 },
|
|
17
|
+
[AnalyticsPeriod.LAST_7_DAYS]: { unit: AnalyticsBucketUnit.DAY, count: 7 },
|
|
18
|
+
[AnalyticsPeriod.LAST_30_DAYS]: { unit: AnalyticsBucketUnit.DAY, count: 30 },
|
|
19
|
+
[AnalyticsPeriod.LAST_12_MONTHS]: { unit: AnalyticsBucketUnit.MONTH, count: 12 }
|
|
20
|
+
};
|
|
21
|
+
function isAnalyticsPeriod(value) {
|
|
22
|
+
return typeof value === "string" && ANALYTICS_PERIODS.includes(value);
|
|
23
|
+
}
|
|
24
|
+
function assertTimeZone(timeZone) {
|
|
25
|
+
zonedFormatter(timeZone);
|
|
26
|
+
}
|
|
27
|
+
function analyticsPeriodRange(period, now, timeZone) {
|
|
28
|
+
const { unit, count } = PERIOD_BUCKETS[period];
|
|
29
|
+
const wall = zonedParts(now.getTime(), timeZone);
|
|
30
|
+
const starts = [];
|
|
31
|
+
for (let back = count - 1; back >= -1; back -= 1) {
|
|
32
|
+
starts.push(wallToInstant(bucketWallStart(unit, wall, back), timeZone));
|
|
33
|
+
}
|
|
34
|
+
const buckets = [];
|
|
35
|
+
for (let index = 0; index < count; index += 1) {
|
|
36
|
+
buckets.push({ start: iso(starts[index]), end: iso(starts[index + 1]) });
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
period,
|
|
40
|
+
unit,
|
|
41
|
+
from: iso(starts[0]),
|
|
42
|
+
to: now.toISOString(),
|
|
43
|
+
buckets
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function bucketWallStart(unit, wall, back) {
|
|
47
|
+
if (unit === AnalyticsBucketUnit.HOUR) {
|
|
48
|
+
return Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour - back);
|
|
49
|
+
}
|
|
50
|
+
if (unit === AnalyticsBucketUnit.DAY) {
|
|
51
|
+
return Date.UTC(wall.year, wall.month - 1, wall.day - back);
|
|
52
|
+
}
|
|
53
|
+
return Date.UTC(wall.year, wall.month - 1 - back, 1);
|
|
54
|
+
}
|
|
55
|
+
function wallToInstant(wall, timeZone) {
|
|
56
|
+
const guess = wall - zoneOffsetMs(wall, timeZone);
|
|
57
|
+
return wall - zoneOffsetMs(guess, timeZone);
|
|
58
|
+
}
|
|
59
|
+
function zoneOffsetMs(instant, timeZone) {
|
|
60
|
+
const parts = zonedParts(instant, timeZone);
|
|
61
|
+
const asUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
62
|
+
return asUtc - Math.floor(instant / 1e3) * 1e3;
|
|
63
|
+
}
|
|
64
|
+
var formatters = /* @__PURE__ */ new Map();
|
|
65
|
+
function zonedFormatter(timeZone) {
|
|
66
|
+
const cached = formatters.get(timeZone);
|
|
67
|
+
if (cached) return cached;
|
|
68
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
69
|
+
timeZone,
|
|
70
|
+
hourCycle: "h23",
|
|
71
|
+
year: "numeric",
|
|
72
|
+
month: "2-digit",
|
|
73
|
+
day: "2-digit",
|
|
74
|
+
hour: "2-digit",
|
|
75
|
+
minute: "2-digit",
|
|
76
|
+
second: "2-digit"
|
|
77
|
+
});
|
|
78
|
+
formatters.set(timeZone, formatter);
|
|
79
|
+
return formatter;
|
|
80
|
+
}
|
|
81
|
+
function zonedParts(instant, timeZone) {
|
|
82
|
+
const parts = {};
|
|
83
|
+
for (const part of zonedFormatter(timeZone).formatToParts(new Date(instant))) {
|
|
84
|
+
if (part.type !== "literal") parts[part.type] = Number(part.value);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
year: parts.year ?? 0,
|
|
88
|
+
month: parts.month ?? 1,
|
|
89
|
+
day: parts.day ?? 1,
|
|
90
|
+
hour: parts.hour ?? 0,
|
|
91
|
+
minute: parts.minute ?? 0,
|
|
92
|
+
second: parts.second ?? 0
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function iso(instant) {
|
|
96
|
+
if (instant === void 0) throw new RangeError("A period bucket has no start");
|
|
97
|
+
return new Date(instant).toISOString();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
AnalyticsPeriod,
|
|
102
|
+
ANALYTICS_PERIODS,
|
|
103
|
+
DEFAULT_ANALYTICS_PERIOD,
|
|
104
|
+
AnalyticsBucketUnit,
|
|
105
|
+
isAnalyticsPeriod,
|
|
106
|
+
assertTimeZone,
|
|
107
|
+
analyticsPeriodRange
|
|
108
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
acceptDurationSeconds,
|
|
3
3
|
summarizeAnalyticsEvents
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-YAC3MRH3.js";
|
|
5
5
|
import {
|
|
6
6
|
AnalyticsEventKind
|
|
7
7
|
} from "./chunk-F7BY2JQ3.js";
|
|
@@ -17,42 +17,85 @@ function analyticsEventsAccess(read) {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// src/payload/admin/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
20
|
+
// src/payload/admin/translations.ts
|
|
21
|
+
var ANALYTICS_TRANSLATION_NAMESPACE = "analytics";
|
|
22
|
+
var ENGLISH_ANALYTICS_LABELS = {
|
|
23
|
+
navGroup: "Statistics",
|
|
24
|
+
navLabel: "Analytics",
|
|
25
|
+
title: "Analytics",
|
|
26
|
+
period: "Period",
|
|
27
|
+
period_24h: "Last 24 hours",
|
|
28
|
+
period_7d: "Last 7 days",
|
|
29
|
+
period_30d: "Last 30 days",
|
|
30
|
+
period_12m: "Last 12 months",
|
|
31
|
+
country: "Country",
|
|
32
|
+
allCountries: "All countries",
|
|
33
|
+
views: "Views",
|
|
34
|
+
averageTime: "Average time on page",
|
|
35
|
+
clicks: "Clicks",
|
|
36
|
+
viewsOverTime: "Views over time",
|
|
37
|
+
topPages: "Top pages",
|
|
38
|
+
allPages: "All pages",
|
|
39
|
+
showAllPages: "Show all pages",
|
|
40
|
+
showTopPages: "Show top pages only",
|
|
41
|
+
page: "Page",
|
|
42
|
+
pageFilter: "Page: {{path}}",
|
|
43
|
+
clearFilter: "Clear",
|
|
44
|
+
countries: "Countries",
|
|
45
|
+
cities: "Cities",
|
|
46
|
+
city: "City",
|
|
47
|
+
event: "Event",
|
|
48
|
+
count: "Count",
|
|
49
|
+
sessions: "Sessions",
|
|
50
|
+
session: "Session",
|
|
51
|
+
started: "Started",
|
|
52
|
+
pages: "Pages",
|
|
53
|
+
sessionEvents: "Session events",
|
|
54
|
+
allSessions: "All sessions",
|
|
55
|
+
time: "Time",
|
|
56
|
+
kind: "Kind",
|
|
57
|
+
kindView: "View",
|
|
58
|
+
kindClick: "Click",
|
|
59
|
+
empty: "Nothing in this period."
|
|
60
|
+
};
|
|
61
|
+
function withAnalyticsTranslations(i18n, translations = {}) {
|
|
62
|
+
const existing = i18n?.translations ?? {};
|
|
63
|
+
const languages = /* @__PURE__ */ new Set([
|
|
64
|
+
"en",
|
|
65
|
+
...Object.keys(i18n?.supportedLanguages ?? {}),
|
|
66
|
+
...Object.keys(translations)
|
|
67
|
+
]);
|
|
68
|
+
const merged = { ...existing };
|
|
69
|
+
for (const language of languages) {
|
|
70
|
+
const current = existing[language] ?? {};
|
|
71
|
+
const host = translations[language] ?? {};
|
|
72
|
+
merged[language] = {
|
|
73
|
+
...current,
|
|
74
|
+
[ANALYTICS_TRANSLATION_NAMESPACE]: {
|
|
75
|
+
...ENGLISH_ANALYTICS_LABELS,
|
|
76
|
+
...host,
|
|
77
|
+
events: { ...host.events }
|
|
78
|
+
}
|
|
79
|
+
};
|
|
39
80
|
}
|
|
40
|
-
return
|
|
81
|
+
return { ...i18n, translations: merged };
|
|
41
82
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
const trimmed = value.trim();
|
|
45
|
-
return trimmed.length === 0 ? null : trimmed;
|
|
83
|
+
function analyticsLabel(t, key, vars) {
|
|
84
|
+
return t(`${ANALYTICS_TRANSLATION_NAMESPACE}:${key}`, vars);
|
|
46
85
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (left.id < right.id) return -1;
|
|
52
|
-
if (left.id > right.id) return 1;
|
|
53
|
-
return 0;
|
|
54
|
-
});
|
|
86
|
+
function analyticsEventLabel(translations, eventName) {
|
|
87
|
+
const namespace = property(translations, ANALYTICS_TRANSLATION_NAMESPACE);
|
|
88
|
+
const label = property(property(namespace, "events"), eventName);
|
|
89
|
+
return typeof label === "string" && label.length > 0 ? label : eventName;
|
|
55
90
|
}
|
|
91
|
+
function property(value, key) {
|
|
92
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
93
|
+
return value[key];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/payload/admin/component-paths.ts
|
|
97
|
+
var ANALYTICS_FILTERS_PATH = "@nodewrite-labs/analytics-sdk/payload/admin-filters#AnalyticsFilters";
|
|
98
|
+
var ANALYTICS_FILTERS_DEPENDENCY = "nodewriteAnalyticsFilters";
|
|
56
99
|
|
|
57
100
|
// src/payload/collection.ts
|
|
58
101
|
var ANALYTICS_EVENTS_SLUG = "analytics-events";
|
|
@@ -242,29 +285,42 @@ function timeText(value) {
|
|
|
242
285
|
|
|
243
286
|
// src/payload/stored-modules.ts
|
|
244
287
|
var ANALYTICS_CUSTOM_KEY = "analytics";
|
|
245
|
-
|
|
288
|
+
var DEFAULT_ANALYTICS_TIME_ZONE = "UTC";
|
|
289
|
+
var MODULES_OFF = { country: false, city: false, sessions: false, clicks: false };
|
|
290
|
+
function analyticsAdminSettingsFromConfig(config) {
|
|
246
291
|
const stored = config.custom?.[ANALYTICS_CUSTOM_KEY];
|
|
247
|
-
if (!
|
|
248
|
-
return {
|
|
292
|
+
if (!isRecord2(stored) || !isModules(stored.modules)) {
|
|
293
|
+
return { modules: MODULES_OFF, timeZone: DEFAULT_ANALYTICS_TIME_ZONE, formatLocales: {} };
|
|
249
294
|
}
|
|
250
|
-
return
|
|
295
|
+
return {
|
|
296
|
+
modules: stored.modules,
|
|
297
|
+
timeZone: typeof stored.timeZone === "string" ? stored.timeZone : DEFAULT_ANALYTICS_TIME_ZONE,
|
|
298
|
+
formatLocales: isStringRecord(stored.formatLocales) ? stored.formatLocales : {}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function isModules(value) {
|
|
302
|
+
if (!isRecord2(value)) return false;
|
|
303
|
+
return typeof value.country === "boolean" && typeof value.city === "boolean" && typeof value.sessions === "boolean" && typeof value.clicks === "boolean";
|
|
251
304
|
}
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
const modules = value.modules;
|
|
255
|
-
if (typeof modules !== "object" || modules === null) return false;
|
|
256
|
-
return hasBoolean(modules, "country") && hasBoolean(modules, "city") && hasBoolean(modules, "sessions") && hasBoolean(modules, "clicks");
|
|
305
|
+
function isStringRecord(value) {
|
|
306
|
+
return isRecord2(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
257
307
|
}
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
return typeof record[key] === "boolean";
|
|
308
|
+
function isRecord2(value) {
|
|
309
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
261
310
|
}
|
|
262
311
|
|
|
263
312
|
export {
|
|
264
313
|
analyticsEventsAccess,
|
|
265
|
-
|
|
314
|
+
ENGLISH_ANALYTICS_LABELS,
|
|
315
|
+
withAnalyticsTranslations,
|
|
316
|
+
analyticsLabel,
|
|
317
|
+
analyticsEventLabel,
|
|
318
|
+
ANALYTICS_FILTERS_PATH,
|
|
319
|
+
ANALYTICS_FILTERS_DEPENDENCY,
|
|
320
|
+
ANALYTICS_EVENTS_SLUG,
|
|
266
321
|
analyticsEventsCollection,
|
|
267
322
|
PayloadAnalyticsEventStore,
|
|
268
323
|
ANALYTICS_CUSTOM_KEY,
|
|
269
|
-
|
|
324
|
+
DEFAULT_ANALYTICS_TIME_ZONE,
|
|
325
|
+
analyticsAdminSettingsFromConfig
|
|
270
326
|
};
|
|
@@ -20,6 +20,9 @@ function summarizeAnalyticsEvents(rows, query, modules) {
|
|
|
20
20
|
averageDurationSeconds: meanDuration(views),
|
|
21
21
|
byPath: groupByPath(views)
|
|
22
22
|
};
|
|
23
|
+
if (query.buckets) {
|
|
24
|
+
summary.viewsOverTime = countViewsByBucket(views, query.buckets);
|
|
25
|
+
}
|
|
23
26
|
if (modules.country) {
|
|
24
27
|
summary.byCountry = groupByCountry(views);
|
|
25
28
|
}
|
|
@@ -61,6 +64,17 @@ function meanDuration(views) {
|
|
|
61
64
|
if (count === 0) return 0;
|
|
62
65
|
return total / count;
|
|
63
66
|
}
|
|
67
|
+
function countViewsByBucket(views, buckets) {
|
|
68
|
+
const counts = buckets.map(() => 0);
|
|
69
|
+
const ends = buckets.map((bucket) => Date.parse(bucket.end));
|
|
70
|
+
const starts = buckets.map((bucket) => Date.parse(bucket.start));
|
|
71
|
+
for (const view of views) {
|
|
72
|
+
const at = Date.parse(view.occurredAt);
|
|
73
|
+
const index = starts.findIndex((start, position) => at >= start && at < (ends[position] ?? start));
|
|
74
|
+
if (index >= 0) counts[index] = (counts[index] ?? 0) + 1;
|
|
75
|
+
}
|
|
76
|
+
return buckets.map((bucket, index) => ({ start: bucket.start, views: counts[index] ?? 0 }));
|
|
77
|
+
}
|
|
64
78
|
function groupByPath(views) {
|
|
65
79
|
const groups = /* @__PURE__ */ new Map();
|
|
66
80
|
for (const view of views) {
|
package/dist/client/index.d.ts
CHANGED
package/dist/client/index.js
CHANGED
|
@@ -199,7 +199,9 @@ async function postJson(url, body) {
|
|
|
199
199
|
return fetch(url, {
|
|
200
200
|
method: "POST",
|
|
201
201
|
headers: { "Content-Type": "application/json" },
|
|
202
|
-
body: JSON.stringify(body)
|
|
202
|
+
body: JSON.stringify(body),
|
|
203
|
+
// A tel: or mailto: click can hand the page to another app before the request finishes.
|
|
204
|
+
keepalive: true
|
|
203
205
|
});
|
|
204
206
|
}
|
|
205
207
|
async function readViewId(response) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AnalyticsModules } from './options-
|
|
1
|
+
import { a as AnalyticsModules } from './options-Dpx2vs4R.js';
|
|
2
2
|
|
|
3
3
|
declare const AnalyticsEventKind: {
|
|
4
4
|
readonly VIEW: "view";
|
|
@@ -21,16 +21,55 @@ interface StoredAnalyticsEvent extends NewAnalyticsEvent {
|
|
|
21
21
|
occurredAt: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
declare const AnalyticsPeriod: {
|
|
25
|
+
readonly LAST_24_HOURS: "24h";
|
|
26
|
+
readonly LAST_7_DAYS: "7d";
|
|
27
|
+
readonly LAST_30_DAYS: "30d";
|
|
28
|
+
readonly LAST_12_MONTHS: "12m";
|
|
29
|
+
};
|
|
30
|
+
type AnalyticsPeriod = (typeof AnalyticsPeriod)[keyof typeof AnalyticsPeriod];
|
|
31
|
+
declare const ANALYTICS_PERIODS: readonly AnalyticsPeriod[];
|
|
32
|
+
declare const DEFAULT_ANALYTICS_PERIOD: AnalyticsPeriod;
|
|
33
|
+
declare const AnalyticsBucketUnit: {
|
|
34
|
+
readonly HOUR: "hour";
|
|
35
|
+
readonly DAY: "day";
|
|
36
|
+
readonly MONTH: "month";
|
|
37
|
+
};
|
|
38
|
+
type AnalyticsBucketUnit = (typeof AnalyticsBucketUnit)[keyof typeof AnalyticsBucketUnit];
|
|
39
|
+
/** One step of the chart. `start` is inclusive and `end` exclusive, both ISO instants. */
|
|
40
|
+
interface AnalyticsBucket {
|
|
41
|
+
start: string;
|
|
42
|
+
end: string;
|
|
43
|
+
}
|
|
44
|
+
interface AnalyticsPeriodRange {
|
|
45
|
+
period: AnalyticsPeriod;
|
|
46
|
+
unit: AnalyticsBucketUnit;
|
|
47
|
+
from: string;
|
|
48
|
+
to: string;
|
|
49
|
+
buckets: AnalyticsBucket[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The period that ends now: the current hour, day or month plus the ones before it, counted on
|
|
53
|
+
* the wall clock of `timeZone`, so a day starts at that zone's midnight.
|
|
54
|
+
*/
|
|
55
|
+
declare function analyticsPeriodRange(period: AnalyticsPeriod, now: Date, timeZone: string): AnalyticsPeriodRange;
|
|
56
|
+
|
|
24
57
|
interface AnalyticsSummaryQuery {
|
|
25
58
|
from: string;
|
|
26
59
|
to: string;
|
|
27
60
|
country: string | null;
|
|
28
61
|
path: string | null;
|
|
29
62
|
sessionId: string | null;
|
|
63
|
+
/** When set, the summary counts views per bucket in `viewsOverTime`. */
|
|
64
|
+
buckets?: readonly AnalyticsBucket[];
|
|
30
65
|
}
|
|
31
66
|
interface AnalyticsSummary {
|
|
32
67
|
totalViews: number;
|
|
33
68
|
averageDurationSeconds: number;
|
|
69
|
+
viewsOverTime?: {
|
|
70
|
+
start: string;
|
|
71
|
+
views: number;
|
|
72
|
+
}[];
|
|
34
73
|
byPath: {
|
|
35
74
|
path: string;
|
|
36
75
|
views: number;
|
|
@@ -68,5 +107,5 @@ interface AnalyticsEventStore {
|
|
|
68
107
|
summarizeAnalytics(query: AnalyticsSummaryQuery, modules: AnalyticsModules): Promise<AnalyticsSummary>;
|
|
69
108
|
}
|
|
70
109
|
|
|
71
|
-
export {
|
|
72
|
-
export type { AnalyticsSummaryQuery as A, NewAnalyticsEvent as N, StoredAnalyticsEvent as S, AnalyticsSummary as a, AnalyticsEventStore as b };
|
|
110
|
+
export { DEFAULT_ANALYTICS_PERIOD as D, ANALYTICS_PERIODS as c, AnalyticsBucketUnit as e, AnalyticsEventKind as f, AnalyticsPeriod as g, analyticsPeriodRange as i };
|
|
111
|
+
export type { AnalyticsSummaryQuery as A, NewAnalyticsEvent as N, StoredAnalyticsEvent as S, AnalyticsSummary as a, AnalyticsEventStore as b, AnalyticsBucket as d, AnalyticsPeriodRange as h };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as StoredAnalyticsEvent, A as AnalyticsSummaryQuery, a as AnalyticsSummary, b as AnalyticsEventStore, N as NewAnalyticsEvent } from './event-store-
|
|
2
|
-
export { c as AnalyticsEventKind } from './event-store-
|
|
3
|
-
import { a as AnalyticsModules } from './options-
|
|
4
|
-
export { A as AnalyticsClientConfig, b as AnalyticsServerOptions } from './options-
|
|
1
|
+
import { S as StoredAnalyticsEvent, A as AnalyticsSummaryQuery, a as AnalyticsSummary, b as AnalyticsEventStore, N as NewAnalyticsEvent } from './event-store-BkSuP4ZE.js';
|
|
2
|
+
export { c as ANALYTICS_PERIODS, d as AnalyticsBucket, e as AnalyticsBucketUnit, f as AnalyticsEventKind, g as AnalyticsPeriod, h as AnalyticsPeriodRange, D as DEFAULT_ANALYTICS_PERIOD, i as analyticsPeriodRange } from './event-store-BkSuP4ZE.js';
|
|
3
|
+
import { a as AnalyticsModules } from './options-Dpx2vs4R.js';
|
|
4
|
+
export { A as AnalyticsClientConfig, b as AnalyticsServerOptions, R as ReadGeoFromHeaders, c as RequestGeo } from './options-Dpx2vs4R.js';
|
|
5
5
|
export { A as AnalyticsEventBody } from './event-body-fpV2mYpY.js';
|
|
6
6
|
|
|
7
7
|
/** Keeps a duration inside 0..1800. NaN and infinities are rejected. */
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANALYTICS_PERIODS,
|
|
3
|
+
AnalyticsBucketUnit,
|
|
4
|
+
AnalyticsPeriod,
|
|
5
|
+
DEFAULT_ANALYTICS_PERIOD,
|
|
6
|
+
analyticsPeriodRange
|
|
7
|
+
} from "./chunk-MOCOVYQK.js";
|
|
1
8
|
import {
|
|
2
9
|
acceptDurationSeconds,
|
|
3
10
|
summarizeAnalyticsEvents
|
|
4
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-YAC3MRH3.js";
|
|
5
12
|
import {
|
|
6
13
|
AnalyticsEventKind
|
|
7
14
|
} from "./chunk-F7BY2JQ3.js";
|
|
@@ -42,8 +49,13 @@ var MemoryAnalyticsEventStore = class {
|
|
|
42
49
|
}
|
|
43
50
|
};
|
|
44
51
|
export {
|
|
52
|
+
ANALYTICS_PERIODS,
|
|
53
|
+
AnalyticsBucketUnit,
|
|
45
54
|
AnalyticsEventKind,
|
|
55
|
+
AnalyticsPeriod,
|
|
56
|
+
DEFAULT_ANALYTICS_PERIOD,
|
|
46
57
|
MemoryAnalyticsEventStore,
|
|
47
58
|
acceptDurationSeconds,
|
|
59
|
+
analyticsPeriodRange,
|
|
48
60
|
summarizeAnalyticsEvents
|
|
49
61
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
interface AnalyticsModules {
|
|
2
|
+
country: boolean;
|
|
3
|
+
city: boolean;
|
|
4
|
+
sessions: boolean;
|
|
5
|
+
clicks: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface AnalyticsServerOptions {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
modules: AnalyticsModules;
|
|
10
|
+
}
|
|
11
|
+
/** Country and city for one request, as the host reports them. */
|
|
12
|
+
interface RequestGeo {
|
|
13
|
+
country: string | null;
|
|
14
|
+
city: string | null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reads the visitor's country and city from the request headers. The app supplies it, because
|
|
18
|
+
* each host names its geo headers differently.
|
|
19
|
+
*/
|
|
20
|
+
type ReadGeoFromHeaders = (headers: Headers) => RequestGeo;
|
|
21
|
+
interface AnalyticsClientConfig {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
locale?: string;
|
|
25
|
+
sessions: boolean;
|
|
26
|
+
clicks: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type { AnalyticsClientConfig as A, ReadGeoFromHeaders as R, AnalyticsModules as a, AnalyticsServerOptions as b, RequestGeo as c };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/** A type alias, not an interface, so it fits Payload's index-signature `Option`. */
|
|
4
|
+
type AnalyticsFilterOption = {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
interface AnalyticsFilter {
|
|
9
|
+
/** The id ties the visible label to its dropdown. */
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
options: AnalyticsFilterOption[];
|
|
13
|
+
value: string;
|
|
14
|
+
/** The report URL for each option's value, built on the server. */
|
|
15
|
+
hrefs: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
interface AnalyticsFiltersProps {
|
|
18
|
+
filters: AnalyticsFilter[];
|
|
19
|
+
}
|
|
20
|
+
/** Payload's own dropdowns. Choosing an option loads that report URL. */
|
|
21
|
+
declare function AnalyticsFilters({ filters }: AnalyticsFiltersProps): react_jsx_runtime.JSX.Element;
|
|
22
|
+
|
|
23
|
+
export { AnalyticsFilters };
|
|
24
|
+
export type { AnalyticsFilter, AnalyticsFilterOption, AnalyticsFiltersProps };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/payload/admin/filters.tsx
|
|
4
|
+
import { ReactSelect } from "@payloadcms/ui";
|
|
5
|
+
import { useRouter } from "next/navigation.js";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
function AnalyticsFilters({ filters }) {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
return /* @__PURE__ */ jsx("div", { className: "nwa-filters", children: filters.map((filter) => /* @__PURE__ */ jsxs("div", { className: "nwa-filter", children: [
|
|
10
|
+
/* @__PURE__ */ jsx("label", { className: "nwa-filter__label", htmlFor: filter.id, children: filter.label }),
|
|
11
|
+
/* @__PURE__ */ jsx(
|
|
12
|
+
ReactSelect,
|
|
13
|
+
{
|
|
14
|
+
inputId: filter.id,
|
|
15
|
+
isClearable: false,
|
|
16
|
+
isSearchable: filter.options.length > 8,
|
|
17
|
+
options: filter.options,
|
|
18
|
+
value: filter.options.find((option) => option.value === filter.value),
|
|
19
|
+
onChange: (selected) => {
|
|
20
|
+
const href = filter.hrefs[optionValue(selected)];
|
|
21
|
+
if (href) router.push(href);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
] }, filter.id)) });
|
|
26
|
+
}
|
|
27
|
+
function optionValue(selected) {
|
|
28
|
+
const option = Array.isArray(selected) ? selected[0] : selected;
|
|
29
|
+
return typeof option?.value === "string" ? option.value : "";
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
AnalyticsFilters
|
|
33
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { AdminViewServerProps } from 'payload';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** The analytics screen inside Payload's own admin frame, sidebar included. */
|
|
5
|
+
declare function AnalyticsAdminView(props: AdminViewServerProps): Promise<react_jsx_runtime.JSX.Element>;
|
|
5
6
|
|
|
6
7
|
export { AnalyticsAdminView };
|