@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.
@@ -1,6 +1,6 @@
1
1
  import { Access, Config, Payload } from 'payload';
2
- import { AnalyticsModules, AnalyticsServerOptions } from '../index.js';
3
- import { AnalyticsSummary, AnalyticsEventStore, NewAnalyticsEvent, StoredAnalyticsEvent, AnalyticsSummaryQuery } from '../index.js';
2
+ import { AnalyticsServerOptions, ReadGeoFromHeaders, AnalyticsModules } from '../index.js';
3
+ import { AnalyticsEventStore, NewAnalyticsEvent, StoredAnalyticsEvent, AnalyticsSummaryQuery, AnalyticsSummary } from '../index.js';
4
4
 
5
5
  /**
6
6
  * Read is allowed for a signed-in user, or by the function the plugin was given.
@@ -13,46 +13,80 @@ declare function analyticsEventsAccess(read?: Access): {
13
13
  delete: Access;
14
14
  };
15
15
 
16
- interface AdminSectionsQuery {
17
- session?: string | null;
18
- }
19
- type AdminSection = {
20
- kind: "totals";
21
- totalViews: number;
22
- averageDurationSeconds: number;
23
- } | {
24
- kind: "paths";
25
- rows: AnalyticsSummary["byPath"];
26
- } | {
27
- kind: "countries";
28
- rows: NonNullable<AnalyticsSummary["byCountry"]>;
29
- } | {
30
- kind: "cities";
31
- rows: NonNullable<AnalyticsSummary["byCity"]>;
32
- } | {
33
- kind: "namedClicks";
34
- rows: NonNullable<AnalyticsSummary["byEventName"]>;
35
- } | {
36
- kind: "sessions";
37
- rows: NonNullable<AnalyticsSummary["sessions"]>;
38
- } | {
39
- kind: "trail";
40
- events: NonNullable<AnalyticsSummary["events"]>;
16
+ /** The English labels, shown in every admin language the host does not translate. */
17
+ declare const ENGLISH_ANALYTICS_LABELS: {
18
+ readonly navGroup: "Statistics";
19
+ readonly navLabel: "Analytics";
20
+ readonly title: "Analytics";
21
+ readonly period: "Period";
22
+ readonly period_24h: "Last 24 hours";
23
+ readonly period_7d: "Last 7 days";
24
+ readonly period_30d: "Last 30 days";
25
+ readonly period_12m: "Last 12 months";
26
+ readonly country: "Country";
27
+ readonly allCountries: "All countries";
28
+ readonly views: "Views";
29
+ readonly averageTime: "Average time on page";
30
+ readonly clicks: "Clicks";
31
+ readonly viewsOverTime: "Views over time";
32
+ readonly topPages: "Top pages";
33
+ readonly allPages: "All pages";
34
+ readonly showAllPages: "Show all pages";
35
+ readonly showTopPages: "Show top pages only";
36
+ readonly page: "Page";
37
+ readonly pageFilter: "Page: {{path}}";
38
+ readonly clearFilter: "Clear";
39
+ readonly countries: "Countries";
40
+ readonly cities: "Cities";
41
+ readonly city: "City";
42
+ readonly event: "Event";
43
+ readonly count: "Count";
44
+ readonly sessions: "Sessions";
45
+ readonly session: "Session";
46
+ readonly started: "Started";
47
+ readonly pages: "Pages";
48
+ readonly sessionEvents: "Session events";
49
+ readonly allSessions: "All sessions";
50
+ readonly time: "Time";
51
+ readonly kind: "Kind";
52
+ readonly kindView: "View";
53
+ readonly kindClick: "Click";
54
+ readonly empty: "Nothing in this period.";
41
55
  };
42
- /**
43
- * Blocks for the admin screen, in display order.
44
- * A module that is off contributes no block.
45
- * A session query replaces the session list with one trail of that session's events, in time order.
46
- */
47
- declare function adminSections(summary: AnalyticsSummary, modules: AnalyticsModules, query: AdminSectionsQuery): AdminSection[];
56
+ type AnalyticsLabelKey = keyof typeof ENGLISH_ANALYTICS_LABELS;
57
+ type AnalyticsLabels = Record<AnalyticsLabelKey, string>;
58
+ /** One admin language's labels. Missing keys show English. `events` names named clicks. */
59
+ type AnalyticsTranslation = Partial<AnalyticsLabels> & {
60
+ events?: Record<string, string>;
61
+ };
62
+ /** Keyed by Payload admin language, such as `en` or `rs-latin`. */
63
+ type AnalyticsTranslations = Record<string, AnalyticsTranslation>;
48
64
 
65
+ interface AnalyticsEndpointOptions extends AnalyticsServerOptions {
66
+ /** Without it, the country and city modules store null. */
67
+ geo?: ReadGeoFromHeaders;
68
+ }
69
+
70
+ interface AnalyticsPluginOptions extends AnalyticsEndpointOptions {
71
+ read?: Access;
72
+ /** IANA time zone the report's hours, days and months follow. Defaults to UTC. */
73
+ timeZone?: string;
74
+ /**
75
+ * Labels per Payload admin language, over the English defaults. A language with no entry
76
+ * shows English, so the package never needs to know the host's languages.
77
+ */
78
+ translations?: AnalyticsTranslations;
79
+ /**
80
+ * The BCP 47 tag per admin language for numbers, dates and country names, for languages whose
81
+ * Payload code is not one (`rs-latin` → `sr-Latn`). Others use Payload's date locale.
82
+ */
83
+ formatLocales?: Record<string, string>;
84
+ }
49
85
  /**
50
- * Adds the analytics collection, the ingest endpoint, and the admin screen.
51
- * Existing collections, endpoints, and views stay in place.
86
+ * Adds the analytics collection, the ingest endpoint, the admin screen, and its sidebar link.
87
+ * Existing collections, endpoints, views, and translations stay in place.
52
88
  */
53
- declare function analyticsPlugin(options: AnalyticsServerOptions & {
54
- read?: Access;
55
- }): (config: Config) => Config;
89
+ declare function analyticsPlugin(options: AnalyticsPluginOptions): (config: Config) => Config;
56
90
 
57
91
  /**
58
92
  * AnalyticsEventStore on Payload's Local API.
@@ -67,4 +101,5 @@ declare class PayloadAnalyticsEventStore implements AnalyticsEventStore {
67
101
  summarizeAnalytics(query: AnalyticsSummaryQuery, modules: AnalyticsModules): Promise<AnalyticsSummary>;
68
102
  }
69
103
 
70
- export { PayloadAnalyticsEventStore, adminSections, analyticsEventsAccess, analyticsPlugin };
104
+ export { ENGLISH_ANALYTICS_LABELS, PayloadAnalyticsEventStore, analyticsEventsAccess, analyticsPlugin };
105
+ export type { AnalyticsEndpointOptions, AnalyticsLabelKey, AnalyticsLabels, AnalyticsPluginOptions, AnalyticsTranslation, AnalyticsTranslations };
@@ -3,18 +3,31 @@ import {
3
3
  } from "../chunk-GWOVVKP3.js";
4
4
  import {
5
5
  ANALYTICS_CUSTOM_KEY,
6
+ ANALYTICS_FILTERS_DEPENDENCY,
7
+ ANALYTICS_FILTERS_PATH,
8
+ DEFAULT_ANALYTICS_TIME_ZONE,
9
+ ENGLISH_ANALYTICS_LABELS,
6
10
  PayloadAnalyticsEventStore,
7
- adminSections,
8
11
  analyticsEventsAccess,
9
- analyticsEventsCollection
10
- } from "../chunk-GVOESWHZ.js";
11
- import "../chunk-Q4JRXZ6M.js";
12
+ analyticsEventsCollection,
13
+ withAnalyticsTranslations
14
+ } from "../chunk-QPHMFHRE.js";
15
+ import {
16
+ assertTimeZone
17
+ } from "../chunk-MOCOVYQK.js";
18
+ import "../chunk-YAC3MRH3.js";
12
19
  import "../chunk-F7BY2JQ3.js";
13
20
 
14
21
  // src/payload/endpoint.ts
22
+ var NO_GEO = { country: null, city: null };
15
23
  async function handleAnalyticsEvent(request, deps) {
16
24
  const result = await recordAnalyticsEvent(
17
- { body: request.body, headers: headerRecord(request.headers) },
25
+ {
26
+ body: request.body,
27
+ headers: headerRecord(request.headers),
28
+ country: request.country ?? null,
29
+ city: request.city ?? null
30
+ },
18
31
  deps
19
32
  );
20
33
  if (result.status === 204 || result.body === null) {
@@ -37,7 +50,11 @@ async function handlePayloadAnalyticsEvent(req, options) {
37
50
  } catch {
38
51
  body = void 0;
39
52
  }
40
- return handleAnalyticsEvent({ body, headers: req.headers }, { store, options });
53
+ const geo = options.geo ? options.geo(req.headers) : NO_GEO;
54
+ return handleAnalyticsEvent(
55
+ { body, headers: req.headers, country: geo.country, city: geo.city },
56
+ { store, options }
57
+ );
41
58
  }
42
59
  function headerRecord(headers) {
43
60
  if (headers instanceof Headers) {
@@ -51,16 +68,32 @@ function headerRecord(headers) {
51
68
  }
52
69
 
53
70
  // src/payload/plugin.ts
71
+ var DEFAULT_ADMIN_ROUTE = "/admin";
72
+ var ANALYTICS_VIEW_PATH = "/analytics";
54
73
  function analyticsPlugin(options) {
74
+ const timeZone = options.timeZone ?? DEFAULT_ANALYTICS_TIME_ZONE;
75
+ assertTimeZone(timeZone);
76
+ const custom = {
77
+ enabled: options.enabled,
78
+ modules: options.modules,
79
+ timeZone,
80
+ formatLocales: options.formatLocales ?? {}
81
+ };
55
82
  return (config) => {
83
+ const adminRoute = config.routes?.admin ?? DEFAULT_ADMIN_ROUTE;
56
84
  const admin = config.admin ?? {};
57
85
  const components = admin.components ?? {};
58
86
  return {
59
87
  ...config,
60
88
  collections: [...config.collections ?? [], analyticsEventsCollection(options.read)],
61
89
  endpoints: [...config.endpoints ?? [], analyticsEventsEndpoint(options)],
90
+ i18n: withAnalyticsTranslations(config.i18n, options.translations),
62
91
  admin: {
63
92
  ...admin,
93
+ dependencies: {
94
+ ...admin.dependencies,
95
+ [ANALYTICS_FILTERS_DEPENDENCY]: { type: "component", path: ANALYTICS_FILTERS_PATH }
96
+ },
64
97
  components: {
65
98
  ...components,
66
99
  views: {
@@ -70,31 +103,29 @@ function analyticsPlugin(options) {
70
103
  path: "@nodewrite-labs/analytics-sdk/payload/admin-view",
71
104
  exportName: "AnalyticsAdminView"
72
105
  },
73
- path: "/analytics"
106
+ path: ANALYTICS_VIEW_PATH
74
107
  }
75
108
  },
76
109
  afterNavLinks: [
77
110
  ...components.afterNavLinks ?? [],
78
111
  {
79
112
  path: "@nodewrite-labs/analytics-sdk/payload/nav-link",
80
- exportName: "AnalyticsNavLink"
113
+ exportName: "AnalyticsNavLink",
114
+ clientProps: { href: `${adminRoute}${ANALYTICS_VIEW_PATH}` }
81
115
  }
82
116
  ]
83
117
  }
84
118
  },
85
119
  custom: {
86
120
  ...config.custom,
87
- [ANALYTICS_CUSTOM_KEY]: {
88
- enabled: options.enabled,
89
- modules: options.modules
90
- }
121
+ [ANALYTICS_CUSTOM_KEY]: custom
91
122
  }
92
123
  };
93
124
  };
94
125
  }
95
126
  export {
127
+ ENGLISH_ANALYTICS_LABELS,
96
128
  PayloadAnalyticsEventStore,
97
- adminSections,
98
129
  analyticsEventsAccess,
99
130
  analyticsPlugin
100
131
  };
@@ -1,5 +1,10 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
 
3
- declare function AnalyticsNavLink(): react_jsx_runtime.JSX.Element;
3
+ interface AnalyticsNavLinkProps {
4
+ /** The analytics view under the app's admin route; the plugin sets it. */
5
+ href: string;
6
+ }
7
+ declare function AnalyticsNavLink({ href }: AnalyticsNavLinkProps): react_jsx_runtime.JSX.Element;
4
8
 
5
9
  export { AnalyticsNavLink };
10
+ export type { AnalyticsNavLinkProps };
@@ -1,7 +1,29 @@
1
+ "use client";
2
+
1
3
  // src/payload/admin/nav-link.tsx
2
- import { jsx } from "react/jsx-runtime";
3
- function AnalyticsNavLink() {
4
- return /* @__PURE__ */ jsx("a", { href: "/admin/analytics", children: "Analytics" });
4
+ import { Link, NavGroup, useTranslation } from "@payloadcms/ui";
5
+ import { usePathname } from "next/navigation.js";
6
+
7
+ // src/payload/admin/translations.ts
8
+ var ANALYTICS_TRANSLATION_NAMESPACE = "analytics";
9
+ function analyticsLabel(t, key, vars) {
10
+ return t(`${ANALYTICS_TRANSLATION_NAMESPACE}:${key}`, vars);
11
+ }
12
+
13
+ // src/payload/admin/nav-link.tsx
14
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
+ var NAV_LINK_CLASS = "nav__link";
16
+ var NAV_LINK_ID = "nav-analytics";
17
+ function AnalyticsNavLink({ href }) {
18
+ const { t } = useTranslation();
19
+ const pathname = usePathname();
20
+ const translate = t;
21
+ const isActive = pathname === href || pathname.startsWith(`${href}/`);
22
+ const label = /* @__PURE__ */ jsxs(Fragment, { children: [
23
+ isActive ? /* @__PURE__ */ jsx("div", { className: "nav__link-indicator" }) : null,
24
+ /* @__PURE__ */ jsx("span", { className: "nav__link-label", children: analyticsLabel(translate, "navLabel") })
25
+ ] });
26
+ return /* @__PURE__ */ jsx(NavGroup, { label: analyticsLabel(translate, "navGroup"), children: pathname === href ? /* @__PURE__ */ jsx("div", { className: NAV_LINK_CLASS, id: NAV_LINK_ID, children: label }) : /* @__PURE__ */ jsx(Link, { className: NAV_LINK_CLASS, href, id: NAV_LINK_ID, prefetch: false, children: label }) });
5
27
  }
6
28
  export {
7
29
  AnalyticsNavLink
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  acceptDurationSeconds,
3
3
  summarizeAnalyticsEvents
4
- } from "../chunk-Q4JRXZ6M.js";
4
+ } from "../chunk-YAC3MRH3.js";
5
5
  import {
6
6
  AnalyticsEventKind
7
7
  } from "../chunk-F7BY2JQ3.js";
@@ -1,10 +1,7 @@
1
- import { AnalyticsEventStore } from '../index.js';
1
+ import { c as RequestGeo } from '../options-Dpx2vs4R.js';
2
2
  import { AnalyticsServerOptions } from '../index.js';
3
+ import { AnalyticsEventStore } from '../index.js';
3
4
 
4
- interface RequestGeo {
5
- country: string | null;
6
- city: string | null;
7
- }
8
5
  /** Reads `country` and `city` from the request object the app passed in. */
9
6
  declare function readRequestGeo(request: object): RequestGeo;
10
7
 
@@ -40,5 +37,5 @@ interface RecordAnalyticsEventResult {
40
37
  */
41
38
  declare function recordAnalyticsEvent(request: AnalyticsEventRequest | Request, deps: RecordAnalyticsEventDeps): Promise<RecordAnalyticsEventResult>;
42
39
 
43
- export { readRequestGeo, recordAnalyticsEvent };
44
- export type { AnalyticsEventRequest, AnalyticsHeaderBag, RecordAnalyticsEventDeps, RecordAnalyticsEventResult, RequestGeo };
40
+ export { RequestGeo, readRequestGeo, recordAnalyticsEvent };
41
+ export type { AnalyticsEventRequest, AnalyticsHeaderBag, RecordAnalyticsEventDeps, RecordAnalyticsEventResult };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodewrite-labs/analytics-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Analytics SDK for page views and named clicks, with Postgres storage and a Payload admin screen.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -43,6 +43,10 @@
43
43
  "./payload/nav-link": {
44
44
  "types": "./dist/payload/nav-link.d.ts",
45
45
  "import": "./dist/payload/nav-link.js"
46
+ },
47
+ "./payload/admin-filters": {
48
+ "types": "./dist/payload/admin-filters.d.ts",
49
+ "import": "./dist/payload/admin-filters.js"
46
50
  }
47
51
  },
48
52
  "files": [
@@ -53,17 +57,29 @@
53
57
  "scripts": {
54
58
  "typecheck": "tsc --noEmit",
55
59
  "test": "vitest run",
56
- "build": "tsup"
60
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsup"
57
61
  },
58
62
  "peerDependencies": {
59
63
  "@payloadcms/next": "*",
64
+ "@payloadcms/ui": "*",
65
+ "next": "*",
60
66
  "payload": "*",
61
- "pg": "*"
67
+ "pg": "*",
68
+ "react": "*"
62
69
  },
63
70
  "peerDependenciesMeta": {
64
71
  "@payloadcms/next": {
65
72
  "optional": true
66
73
  },
74
+ "@payloadcms/ui": {
75
+ "optional": true
76
+ },
77
+ "next": {
78
+ "optional": true
79
+ },
80
+ "react": {
81
+ "optional": true
82
+ },
67
83
  "payload": {
68
84
  "optional": true
69
85
  },
@@ -73,18 +89,25 @@
73
89
  },
74
90
  "devDependencies": {
75
91
  "@payloadcms/next": "3.92.0",
92
+ "@payloadcms/ui": "3.92.0",
76
93
  "@types/node": "^22.20.5",
77
94
  "@types/pg": "^8.23.1",
78
95
  "@types/react": "19.2.14",
96
+ "@types/react-dom": "19.2.3",
79
97
  "esbuild": "^0.28.2",
80
98
  "graphql": "^16.14.2",
81
99
  "jsdom": "^30.1.3",
100
+ "next": "16.3.3",
82
101
  "payload": "3.92.0",
83
102
  "pg": "^8.23.1",
84
103
  "react": "^19.3.1",
104
+ "react-dom": "19.3.1",
85
105
  "tsup": "^9.0.0",
86
106
  "typescript": "6.0.3",
87
107
  "vite": "^8.3.5",
88
108
  "vitest": "^5.0.5"
109
+ },
110
+ "dependencies": {
111
+ "country-flag-icons": "1.6.20"
89
112
  }
90
113
  }
@@ -1,12 +1,12 @@
1
- CREATE TABLE IF NOT EXISTS analytics_event (
2
- id uuid primary key,
3
- kind text not null,
4
- path text not null,
5
- locale text,
6
- occurred_at timestamptz not null,
7
- duration_seconds integer,
8
- country text,
9
- city text,
10
- session_id text,
11
- event_name text
12
- );
1
+ CREATE TABLE IF NOT EXISTS analytics_event (
2
+ id uuid primary key,
3
+ kind text not null,
4
+ path text not null,
5
+ locale text,
6
+ occurred_at timestamptz not null,
7
+ duration_seconds integer,
8
+ country text,
9
+ city text,
10
+ session_id text,
11
+ event_name text
12
+ );
@@ -1,19 +0,0 @@
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
- interface AnalyticsClientConfig {
12
- endpoint: string;
13
- enabled: boolean;
14
- locale?: string;
15
- sessions: boolean;
16
- clicks: boolean;
17
- }
18
-
19
- export type { AnalyticsClientConfig as A, AnalyticsModules as a, AnalyticsServerOptions as b };