@endora-commerce/mod-analytics 0.100.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/admin/index.d.ts +28 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +46 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/AnalyticsPage.d.ts +3 -0
  8. package/dist/admin/pages/AnalyticsPage.d.ts.map +1 -0
  9. package/dist/admin/pages/AnalyticsPage.js +58 -0
  10. package/dist/admin/pages/AnalyticsPage.js.map +1 -0
  11. package/dist/backend/entities/analytics-event.entity.d.ts +30 -0
  12. package/dist/backend/entities/analytics-event.entity.d.ts.map +1 -0
  13. package/dist/backend/entities/analytics-event.entity.js +89 -0
  14. package/dist/backend/entities/analytics-event.entity.js.map +1 -0
  15. package/dist/backend/index.d.ts +42 -0
  16. package/dist/backend/index.d.ts.map +1 -0
  17. package/dist/backend/index.js +39 -0
  18. package/dist/backend/index.js.map +1 -0
  19. package/dist/backend/plugin.d.ts +24 -0
  20. package/dist/backend/plugin.d.ts.map +1 -0
  21. package/dist/backend/plugin.js +20 -0
  22. package/dist/backend/plugin.js.map +1 -0
  23. package/dist/backend/routes.d.ts +11 -0
  24. package/dist/backend/routes.d.ts.map +1 -0
  25. package/dist/backend/routes.js +22 -0
  26. package/dist/backend/routes.js.map +1 -0
  27. package/dist/backend/services/analytics-ingest.service.d.ts +27 -0
  28. package/dist/backend/services/analytics-ingest.service.d.ts.map +1 -0
  29. package/dist/backend/services/analytics-ingest.service.js +56 -0
  30. package/dist/backend/services/analytics-ingest.service.js.map +1 -0
  31. package/dist/backend/services/analytics-query.service.d.ts +19 -0
  32. package/dist/backend/services/analytics-query.service.d.ts.map +1 -0
  33. package/dist/backend/services/analytics-query.service.js +51 -0
  34. package/dist/backend/services/analytics-query.service.js.map +1 -0
  35. package/dist/backend/services/ga4-forwarder.d.ts +49 -0
  36. package/dist/backend/services/ga4-forwarder.d.ts.map +1 -0
  37. package/dist/backend/services/ga4-forwarder.js +86 -0
  38. package/dist/backend/services/ga4-forwarder.js.map +1 -0
  39. package/dist/manifest.d.ts +169 -0
  40. package/dist/manifest.d.ts.map +1 -0
  41. package/dist/manifest.js +64 -0
  42. package/dist/manifest.js.map +1 -0
  43. package/dist/migrations/20260425T143139_analytics_init.d.ts +13 -0
  44. package/dist/migrations/20260425T143139_analytics_init.d.ts.map +1 -0
  45. package/dist/migrations/20260425T143139_analytics_init.js +34 -0
  46. package/dist/migrations/20260425T143139_analytics_init.js.map +1 -0
  47. package/dist/migrations/20260912T125655_analytics_events_tenant_scope_indexes.d.ts +31 -0
  48. package/dist/migrations/20260912T125655_analytics_events_tenant_scope_indexes.d.ts.map +1 -0
  49. package/dist/migrations/20260912T125655_analytics_events_tenant_scope_indexes.js +37 -0
  50. package/dist/migrations/20260912T125655_analytics_events_tenant_scope_indexes.js.map +1 -0
  51. package/dist/migrations/index.d.ts +27 -0
  52. package/dist/migrations/index.d.ts.map +1 -0
  53. package/dist/migrations/index.js +30 -0
  54. package/dist/migrations/index.js.map +1 -0
  55. package/docs/analytics.md +74 -0
  56. package/i18n/en.json +19 -0
  57. package/i18n/pl.json +19 -0
  58. package/package.json +92 -0
  59. package/tailwind.css +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Endora sp. z o.o. and the Endora Commerce contributors
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 ADDED
@@ -0,0 +1,57 @@
1
+ <!-- Generated by `pnpm --filter backend run manifests:generate`. Delete this line to take the file over; a README without it is never regenerated. -->
2
+
3
+ # @endora-commerce/mod-analytics
4
+
5
+ Server-side analytics aggregation and dashboard data feeds.
6
+
7
+ ## What this is
8
+
9
+ An **Endora Commerce module package**. Its module id is `analytics` — the identity of record in the platform's registries, in its migrations and in its setting, permission and translation codes.
10
+
11
+ A module is not imported by application code. The platform discovers the extension packages an instance has installed, reads the manifest on the root subpath and composes the module from it, so installing this package and starting the instance is the whole integration.
12
+
13
+ ## Entry points
14
+
15
+ | Import | Contents |
16
+ | --- | --- |
17
+ | `@endora-commerce/mod-analytics` | the module manifest — its id, version, dependencies, settings and activation |
18
+ | `@endora-commerce/mod-analytics/admin` | the Admin UI contribution: the screens, navigation and palette actions it adds |
19
+ | `@endora-commerce/mod-analytics/backend` | the composition root the platform calls, with the entities, services, routes and workers it registers |
20
+ | `@endora-commerce/mod-analytics/migrations` | the module’s own schema migrations, in the order the platform runs them |
21
+ | `@endora-commerce/mod-analytics/tailwind.css` | the Tailwind source declaration for the admin code above |
22
+
23
+ ## Depends on
24
+
25
+ Everything below is a **peer** dependency, so the application resolves exactly one copy of each — two copies of a host or of React are a runtime failure rather than a type error. An entry marked *optional* is needed only by the layers that use it.
26
+
27
+ **Endora packages**
28
+
29
+ - `@endora-commerce/admin-kit` — *optional*
30
+ - `@endora-commerce/contracts`
31
+ - `@endora-commerce/platform`
32
+
33
+ **Third-party**
34
+
35
+ - `@mikro-orm/core` ^6
36
+ - `@mikro-orm/migrations` ^6
37
+ - `@mikro-orm/postgresql` ^6
38
+ - `fastify` ^5
39
+ - `lucide-react` ^1 — *optional*
40
+ - `react` ^19 — *optional*
41
+
42
+ ## What the tarball carries
43
+
44
+ - `dist/` — the compiled JavaScript and its type declarations
45
+ - `i18n/` — the translation bundles this package ships (`en`, `pl`), loaded by the platform
46
+ - `docs/` — the package’s own pages in the Endora Commerce documentation site
47
+ - `tailwind.css` — the Tailwind source declaration
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pnpm add @endora-commerce/mod-analytics
53
+ ```
54
+
55
+ ## Licence
56
+
57
+ MIT — the text is in `LICENSE`, beside this file.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `analytics`' admin surface — one route and one sidebar entry, declared by the
3
+ * module that owns them (feature 091, Phase 4;
4
+ * `specs/091-module-owned-admin-surfaces/contracts/admin-contribution.md`).
5
+ *
6
+ * This is the **second batch** of the drain, and it was selected by the rule
7
+ * `research.md` §6 sets — ascending incoming cross-module reach — with the
8
+ * feasibility test batch one applied on top of it. `analytics` has **zero**
9
+ * admin cross-module reach in either direction, and it is the only remaining
10
+ * zero-incoming directory every one of whose host symbols
11
+ * `@endora-commerce/admin-kit` publishes: the other candidates at that rung
12
+ * reach either a Group A picker (`api_keys`, `comparisons`, `megamenu`, `blog`,
13
+ * `webhooks`) or the Group B session cluster (`payment_methods`, `promotions`),
14
+ * both of which `backend/scripts/ledgers/admin-surface.ts` records as
15
+ * unpublished with a retiring condition that is somebody else's merge request.
16
+ *
17
+ * **This entry exports data and nothing else** (R2), like every `./admin`
18
+ * layer: `check:module-boundary`'s D-171 rule designates a subpath as contract
19
+ * surface when it emits no runtime binding, and `./admin` deliberately does not
20
+ * qualify, so a consumer reaching into another module's `./admin` stays a
21
+ * counted boundary reach.
22
+ *
23
+ * **The component is a dynamic-import factory** (R6), so Vite has a split point
24
+ * whether or not anybody remembers to ask for one.
25
+ */
26
+ import type { AdminContributions } from '@endora-commerce/admin-kit/contributions';
27
+ export declare const contributions: AdminContributions;
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0CAA0C,CAAC;AAKnF,eAAO,MAAM,aAAa,EAAE,kBA0C3B,CAAC"}
@@ -0,0 +1,46 @@
1
+ /** The module's only route: the analytics dashboard. */
2
+ const ROUTE_PATH = '/analytics';
3
+ export const contributions = {
4
+ routes: [
5
+ {
6
+ path: ROUTE_PATH,
7
+ component: () => import('./pages/AnalyticsPage.js'),
8
+ // Read from the route this screen calls —
9
+ // `requireAdmin('analytics:read')` on
10
+ // `GET /api/v1/admin/analytics/summary` — and the same code the
11
+ // module's `open-analytics` palette action declares, which
12
+ // `check:action-route-permissions` holds to that route.
13
+ requiredPermission: 'analytics:read',
14
+ index: true,
15
+ },
16
+ ],
17
+ nav: [
18
+ {
19
+ to: ROUTE_PATH,
20
+ // Module-relative (R8), resolved in this module's own namespace out of
21
+ // `packages/modules/analytics/i18n/`. It was `appShell.nav.analytics` in
22
+ // the shared `_i18n` bundle, one of the four shared files a module author
23
+ // had to edit.
24
+ labelKey: 'nav.analytics.label',
25
+ // The icon the hand-written NAV entry carried. `LineChart` joins
26
+ // `KnownIconNameSchema` and `icon-map.ts` in this merge request so the
27
+ // sidebar renders the same glyph it did before the move; swapping to a
28
+ // name already on the allowlist would have been a visible change bought
29
+ // for nothing.
30
+ icon: 'LineChart',
31
+ section: 'analyticsAds',
32
+ // Below `google_analytics`' 200, which is the order the two had in the
33
+ // hand-written NAV: `/analytics` first, `/google-analytics` second. A
34
+ // module's entries still append to their section as a block until the
35
+ // host's own entries carry weights, so the pair now sits after
36
+ // `/linkedin-ads` and `/meta-ads` rather than before them — recorded in
37
+ // `admin/test/components/AppShell.analytics-nav.test.tsx` rather than
38
+ // left to be discovered.
39
+ weight: 100,
40
+ // The code that opens the screen, which is the only code this module's
41
+ // admin surface enforces — the dashboard is read-only.
42
+ requiredPermission: 'analytics:read',
43
+ },
44
+ ],
45
+ };
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AA2BA,wDAAwD;AACxD,MAAM,UAAU,GAAG,YAAY,CAAC;AAEhC,MAAM,CAAC,MAAM,aAAa,GAAuB;IAC/C,MAAM,EAAE;QACN;YACE,IAAI,EAAE,UAAU;YAChB,SAAS,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,0BAA0B,CAAC;YACnD,0CAA0C;YAC1C,sCAAsC;YACtC,gEAAgE;YAChE,2DAA2D;YAC3D,wDAAwD;YACxD,kBAAkB,EAAE,gBAAgB;YACpC,KAAK,EAAE,IAAI;SACZ;KACF;IACD,GAAG,EAAE;QACH;YACE,EAAE,EAAE,UAAU;YACd,uEAAuE;YACvE,yEAAyE;YACzE,0EAA0E;YAC1E,eAAe;YACf,QAAQ,EAAE,qBAAqB;YAC/B,iEAAiE;YACjE,uEAAuE;YACvE,uEAAuE;YACvE,wEAAwE;YACxE,eAAe;YACf,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,cAAc;YACvB,uEAAuE;YACvE,sEAAsE;YACtE,sEAAsE;YACtE,+DAA+D;YAC/D,wEAAwE;YACxE,sEAAsE;YACtE,yBAAyB;YACzB,MAAM,EAAE,GAAG;YACX,uEAAuE;YACvE,uDAAuD;YACvD,kBAAkB,EAAE,gBAAgB;SACrC;KACF;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type ReactNode } from 'react';
2
+ export default function AnalyticsPage(): ReactNode;
3
+ //# sourceMappingURL=AnalyticsPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnalyticsPage.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/AnalyticsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA6C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AA6BlF,MAAM,CAAC,OAAO,UAAU,aAAa,IAAI,SAAS,CA2JjD"}
@@ -0,0 +1,58 @@
1
+ import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import { RefreshCw } from 'lucide-react';
4
+ import { ApiError, apiClient, formatDateTime } from '@endora-commerce/admin-kit/lib';
5
+ import { Alert, AlertDescription, Button, Card, CardContent, CardHeader, CardTitle, PageHeader, Select, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@endora-commerce/admin-kit/ui';
6
+ import { useTranslation } from '@endora-commerce/admin-kit/i18n';
7
+ const RANGE_OPTION_VALUES = [7, 30, 90];
8
+ export default function AnalyticsPage() {
9
+ const t = useTranslation('analytics');
10
+ const RANGE_OPTIONS = [
11
+ { value: 7, label: t('range.7days') },
12
+ { value: 30, label: t('range.30days') },
13
+ { value: 90, label: t('range.90days') },
14
+ ];
15
+ void RANGE_OPTION_VALUES;
16
+ const [rangeDays, setRangeDays] = useState(30);
17
+ const [summary, setSummary] = useState(null);
18
+ const [loading, setLoading] = useState(true);
19
+ const [error, setError] = useState(null);
20
+ const window = useMemo(() => {
21
+ const to = new Date();
22
+ const from = new Date(to.getTime() - rangeDays * 24 * 60 * 60 * 1000);
23
+ return { fromIso: from.toISOString(), toIso: to.toISOString() };
24
+ }, [rangeDays]);
25
+ const refresh = useCallback(async () => {
26
+ setLoading(true);
27
+ setError(null);
28
+ try {
29
+ const qs = `?from=${encodeURIComponent(window.fromIso)}&to=${encodeURIComponent(window.toIso)}`;
30
+ const res = await apiClient.get(`/api/v1/admin/analytics/summary${qs}`);
31
+ setSummary(res.data);
32
+ }
33
+ catch (err) {
34
+ setError(err instanceof ApiError ? err.envelope.error.message : t('error.load'));
35
+ }
36
+ finally {
37
+ setLoading(false);
38
+ }
39
+ }, [window.fromIso, window.toIso, t]);
40
+ useEffect(() => {
41
+ void refresh();
42
+ }, [refresh]);
43
+ const dailyByDay = useMemo(() => groupDailyByDay(summary?.daily ?? []), [summary]);
44
+ const allTypes = useMemo(() => Array.from(new Set((summary?.daily ?? []).map((d) => d.type))).sort(), [summary]);
45
+ return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { title: t('page.title'), description: _jsxs(_Fragment, { children: [t('page.descriptionPrefix'), " ", formatDateTime(window.fromIso), " \u2013", ' ', formatDateTime(window.toIso), "."] }), actions: _jsxs(_Fragment, { children: [_jsx(Select, { className: "w-auto", value: rangeDays, onChange: (e) => setRangeDays(Number(e.target.value)), children: RANGE_OPTIONS.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
46
+ void refresh();
47
+ }, children: [_jsx(RefreshCw, {}), t('refresh')] })] }) }), error ? (_jsx(Alert, { variant: "destructive", className: "mb-4", children: _jsx(AlertDescription, { children: error }) })) : null, _jsxs(Card, { className: "mb-4", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t('totals.title') }) }), _jsx(CardContent, { children: loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('loading') })) : !summary || summary.totalsByType.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('empty') })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t('column.eventType') }), _jsx(TableHead, { className: "text-right", children: t('column.count') })] }) }), _jsx(TableBody, { children: summary.totalsByType.map((row) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono text-xs", children: row.type }), _jsx(TableCell, { className: "text-right", children: row.count.toLocaleString() })] }, row.type))) })] })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t('daily.title') }) }), _jsx(CardContent, { children: loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('loading') })) : dailyByDay.size === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('empty') })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t('column.day') }), allTypes.map((type) => (_jsx(TableHead, { className: "text-right font-mono text-xs", children: type }, type)))] }) }), _jsx(TableBody, { children: Array.from(dailyByDay.entries()).map(([day, counts]) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: day }), allTypes.map((type) => (_jsx(TableCell, { className: "text-right", children: (counts[type] ?? 0).toLocaleString() }, type)))] }, day))) })] })) })] })] }));
48
+ }
49
+ function groupDailyByDay(rows) {
50
+ const out = new Map();
51
+ for (const row of rows) {
52
+ const bucket = out.get(row.day) ?? {};
53
+ bucket[row.type] = row.count;
54
+ out.set(row.day, bucket);
55
+ }
56
+ return out;
57
+ }
58
+ //# sourceMappingURL=AnalyticsPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnalyticsPage.js","sourceRoot":"","sources":["../../../src/admin/pages/AnalyticsPage.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { OptionalProps } from '@mikro-orm/core';
2
+ /**
3
+ * AnalyticsEvent (T237 / FR-110).
4
+ *
5
+ * Append-only event log. Storefront and admin POST batches into
6
+ * `/api/v1/analytics/events`; the admin dashboard reads aggregated
7
+ * counts from this table. The optional GA4 forwarder consumes the same
8
+ * stream when the feature flag is on.
9
+ *
10
+ * Indexes:
11
+ * - `(occurred_at)` — time-range queries dominate the dashboard
12
+ * - `(type, occurred_at)` — per-type time series
13
+ * - `(sales_channel_id)` — per-channel breakdowns
14
+ */
15
+ export declare class AnalyticsEvent {
16
+ [OptionalProps]?: 'id' | 'occurredAt' | 'recordedAt' | 'salesChannelId' | 'customerAccountId' | 'organizationId' | 'sessionId' | 'properties' | 'requestId';
17
+ id: string;
18
+ type: string;
19
+ /** Storefront-supplied event timestamp; falls back to server `recordedAt`. */
20
+ occurredAt: Date;
21
+ recordedAt: Date;
22
+ salesChannelId?: string | null;
23
+ customerAccountId?: string | null;
24
+ organizationId?: string | null;
25
+ sessionId?: string | null;
26
+ properties?: Record<string, unknown> | null;
27
+ /** Mirror of the X-Request-Id header for cross-log correlation. */
28
+ requestId?: string | null;
29
+ }
30
+ //# sourceMappingURL=analytics-event.entity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics-event.entity.d.ts","sourceRoot":"","sources":["../../../src/backend/entities/analytics-event.entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,aAAa,EAAwB,MAAM,iBAAiB,CAAC;AAIrF;;;;;;;;;;;;GAYG;AACH,qBAGa,cAAc;IACzB,CAAC,aAAa,CAAC,CAAC,EACZ,IAAI,GACJ,YAAY,GACZ,YAAY,GACZ,gBAAgB,GAChB,mBAAmB,GACnB,gBAAgB,GAChB,WAAW,GACX,YAAY,GACZ,WAAW,CAAC;IAGhB,EAAE,EAAE,MAAM,CAAgB;IAG1B,IAAI,EAAG,MAAM,CAAC;IAEd,8EAA8E;IAG9E,UAAU,EAAE,IAAI,CAAc;IAG9B,UAAU,EAAE,IAAI,CAAc;IAI9B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAG/B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAGlC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAG/B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAG1B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAE5C,mEAAmE;IAEnE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B"}
@@ -0,0 +1,89 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Entity, Index, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
11
+ import { OrgScoped } from '@endora-commerce/platform/tenancy';
12
+ import { randomUUID } from 'crypto';
13
+ /**
14
+ * AnalyticsEvent (T237 / FR-110).
15
+ *
16
+ * Append-only event log. Storefront and admin POST batches into
17
+ * `/api/v1/analytics/events`; the admin dashboard reads aggregated
18
+ * counts from this table. The optional GA4 forwarder consumes the same
19
+ * stream when the feature flag is on.
20
+ *
21
+ * Indexes:
22
+ * - `(occurred_at)` — time-range queries dominate the dashboard
23
+ * - `(type, occurred_at)` — per-type time series
24
+ * - `(sales_channel_id)` — per-channel breakdowns
25
+ */
26
+ let AnalyticsEvent = class AnalyticsEvent {
27
+ [OptionalProps];
28
+ id = randomUUID();
29
+ type;
30
+ /** Storefront-supplied event timestamp; falls back to server `recordedAt`. */
31
+ occurredAt = new Date();
32
+ recordedAt = new Date();
33
+ salesChannelId;
34
+ customerAccountId;
35
+ organizationId;
36
+ sessionId;
37
+ properties;
38
+ /** Mirror of the X-Request-Id header for cross-log correlation. */
39
+ requestId;
40
+ };
41
+ __decorate([
42
+ PrimaryKey({ type: 'uuid' }),
43
+ __metadata("design:type", String)
44
+ ], AnalyticsEvent.prototype, "id", void 0);
45
+ __decorate([
46
+ Property({ type: 'string', length: 64 }),
47
+ __metadata("design:type", String)
48
+ ], AnalyticsEvent.prototype, "type", void 0);
49
+ __decorate([
50
+ Property({ type: 'datetime' }),
51
+ Index(),
52
+ __metadata("design:type", Date)
53
+ ], AnalyticsEvent.prototype, "occurredAt", void 0);
54
+ __decorate([
55
+ Property({ type: 'datetime', onCreate: () => new Date() }),
56
+ __metadata("design:type", Date)
57
+ ], AnalyticsEvent.prototype, "recordedAt", void 0);
58
+ __decorate([
59
+ Property({ type: 'uuid', nullable: true }),
60
+ Index(),
61
+ __metadata("design:type", Object)
62
+ ], AnalyticsEvent.prototype, "salesChannelId", void 0);
63
+ __decorate([
64
+ Property({ type: 'uuid', nullable: true }),
65
+ __metadata("design:type", Object)
66
+ ], AnalyticsEvent.prototype, "customerAccountId", void 0);
67
+ __decorate([
68
+ Property({ type: 'uuid', nullable: true }),
69
+ __metadata("design:type", Object)
70
+ ], AnalyticsEvent.prototype, "organizationId", void 0);
71
+ __decorate([
72
+ Property({ type: 'string', length: 64, nullable: true }),
73
+ __metadata("design:type", Object)
74
+ ], AnalyticsEvent.prototype, "sessionId", void 0);
75
+ __decorate([
76
+ Property({ type: 'json', nullable: true }),
77
+ __metadata("design:type", Object)
78
+ ], AnalyticsEvent.prototype, "properties", void 0);
79
+ __decorate([
80
+ Property({ type: 'string', length: 64, nullable: true }),
81
+ __metadata("design:type", Object)
82
+ ], AnalyticsEvent.prototype, "requestId", void 0);
83
+ AnalyticsEvent = __decorate([
84
+ OrgScoped(),
85
+ Entity({ tableName: 'analytics_events' }),
86
+ Index({ name: 'idx_analytics_events_type_occurred_at', properties: ['type', 'occurredAt'] })
87
+ ], AnalyticsEvent);
88
+ export { AnalyticsEvent };
89
+ //# sourceMappingURL=analytics-event.entity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics-event.entity.js","sourceRoot":"","sources":["../../../src/backend/entities/analytics-event.entity.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC;;;;;;;;;;;;GAYG;AAII,IAAM,cAAc,GAApB,MAAM,cAAc;IACzB,CAAC,aAAa,CAAC,CASC;IAGhB,EAAE,GAAW,UAAU,EAAE,CAAC;IAG1B,IAAI,CAAU;IAEd,8EAA8E;IAG9E,UAAU,GAAS,IAAI,IAAI,EAAE,CAAC;IAG9B,UAAU,GAAS,IAAI,IAAI,EAAE,CAAC;IAI9B,cAAc,CAAiB;IAG/B,iBAAiB,CAAiB;IAGlC,cAAc,CAAiB;IAG/B,SAAS,CAAiB;IAG1B,UAAU,CAAkC;IAE5C,mEAAmE;IAEnE,SAAS,CAAiB;CAC3B,CAAA;AAhCC;IADC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;0CACH;AAG1B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;;4CAC3B;AAKd;IAFC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC9B,KAAK,EAAE;8BACI,IAAI;kDAAc;AAG9B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;8BAC/C,IAAI;kDAAc;AAI9B;IAFC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1C,KAAK,EAAE;;sDACuB;AAG/B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;yDACT;AAGlC;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;sDACZ;AAG/B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;iDAC/B;AAG1B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;kDACC;AAI5C;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;iDAC/B;AA5Cf,cAAc;IAH1B,SAAS,EAAE;IACX,MAAM,CAAC,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC;IACzC,KAAK,CAAC,EAAE,IAAI,EAAE,uCAAuC,EAAE,UAAU,EAAE,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,CAAC;GAChF,cAAc,CA6C1B"}
@@ -0,0 +1,42 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { ModuleContext } from '@endora-commerce/platform/kernel';
3
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
4
+ import { AnalyticsIngestService } from './services/analytics-ingest.service.js';
5
+ import { AnalyticsQueryService } from './services/analytics-query.service.js';
6
+ import { type AnalyticsForwarder } from './services/ga4-forwarder.js';
7
+ import { AnalyticsEvent } from './entities/analytics-event.entity.js';
8
+ /**
9
+ * `analytics` — the first wave-1 module whose conversion is only a conversion
10
+ * (feature 072, wave 1).
11
+ *
12
+ * No defect fell out of it and no other module had to move, which is worth
13
+ * noting because the two before it both spilled: `auth` pulled `admin_roles`
14
+ * in, and `currencies` pulled `dictionaries` and `languages`. This one owns its
15
+ * routes, owns its services, and nothing outside reads its handle — the shape
16
+ * the remaining sweep is hoping for.
17
+ *
18
+ * The GA4 forwarder is registered rather than defaulted inside the factory, so
19
+ * a test overrides it by re-registering the name instead of by passing an
20
+ * option through a factory it does not otherwise care about.
21
+ */
22
+ export interface AnalyticsCradle {
23
+ readonly emFactory: () => EntityManager;
24
+ readonly requireAdmin: RequireAdminFactory;
25
+ readonly analyticsForwarder: AnalyticsForwarder;
26
+ readonly analyticsIngestService: AnalyticsIngestService;
27
+ readonly analyticsQueryService: AnalyticsQueryService;
28
+ }
29
+ export declare function registerModule(ctx: ModuleContext): void;
30
+ /**
31
+ * The module's persisted entity classes, on the `./backend` subpath, as one
32
+ * array and **no named class export** (D-168).
33
+ *
34
+ * This is the shape the platform reads when the package is *installed*: the
35
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
36
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
37
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
38
+ * the package pass of `check-entity-tenant-classification`. A missing array is
39
+ * answered with `[]` — zero entities registered, no error anywhere.
40
+ */
41
+ export declare const entities: (typeof AnalyticsEvent)[];
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAyB,KAAK,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAEtE;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAChD,QAAQ,CAAC,sBAAsB,EAAE,sBAAsB,CAAC;IACxD,QAAQ,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;CACvD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAyBvD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,2BAEpB,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { registerAnalyticsRoutes } from './routes.js';
2
+ import { AnalyticsIngestService } from './services/analytics-ingest.service.js';
3
+ import { AnalyticsQueryService } from './services/analytics-query.service.js';
4
+ import { buildForwarderFromEnv } from './services/ga4-forwarder.js';
5
+ import { AnalyticsEvent } from './entities/analytics-event.entity.js';
6
+ export function registerModule(ctx) {
7
+ ctx.di.register({
8
+ analyticsForwarder: ctx.asFunction(() => buildForwarderFromEnv(process.env)).singleton(),
9
+ analyticsIngestService: ctx
10
+ .asFunction(({ emFactory, analyticsForwarder }) => new AnalyticsIngestService(emFactory, analyticsForwarder))
11
+ .singleton(),
12
+ analyticsQueryService: ctx
13
+ .asFunction(({ emFactory }) => new AnalyticsQueryService(emFactory))
14
+ .singleton(),
15
+ });
16
+ ctx.routes(async (app) => {
17
+ const { analyticsIngestService, analyticsQueryService, requireAdmin } = ctx.cradle();
18
+ await registerAnalyticsRoutes(app, {
19
+ ingestService: analyticsIngestService,
20
+ queryService: analyticsQueryService,
21
+ requireAdmin,
22
+ });
23
+ });
24
+ }
25
+ /**
26
+ * The module's persisted entity classes, on the `./backend` subpath, as one
27
+ * array and **no named class export** (D-168).
28
+ *
29
+ * This is the shape the platform reads when the package is *installed*: the
30
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
31
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
32
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
33
+ * the package pass of `check-entity-tenant-classification`. A missing array is
34
+ * answered with `[]` — zero entities registered, no error anywhere.
35
+ */
36
+ export const entities = [
37
+ AnalyticsEvent,
38
+ ];
39
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAA2B,MAAM,6BAA6B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAyBtE,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC;QACd,kBAAkB,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE;QAExF,sBAAsB,EAAE,GAAG;aACxB,UAAU,CACT,CAAC,EAAE,SAAS,EAAE,kBAAkB,EAAmB,EAAE,EAAE,CACrD,IAAI,sBAAsB,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAC5D;aACA,SAAS,EAAE;QAEd,qBAAqB,EAAE,GAAG;aACvB,UAAU,CAAC,CAAC,EAAE,SAAS,EAAmB,EAAE,EAAE,CAAC,IAAI,qBAAqB,CAAC,SAAS,CAAC,CAAC;aACpF,SAAS,EAAE;KACf,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,YAAY,EAAE,GACnE,GAAG,CAAC,MAAM,EAAmB,CAAC;QAChC,MAAM,uBAAuB,CAAC,GAAG,EAAE;YACjC,aAAa,EAAE,sBAAsB;YACrC,YAAY,EAAE,qBAAqB;YACnC,YAAY;SACb,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,cAAc;CACf,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ import { AnalyticsIngestService } from './services/analytics-ingest.service.js';
4
+ import { AnalyticsQueryService } from './services/analytics-query.service.js';
5
+ import { type AnalyticsForwarder } from './services/ga4-forwarder.js';
6
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
7
+ export interface AnalyticsModuleOptions {
8
+ emFactory: () => EntityManager;
9
+ requireAdmin: RequireAdminFactory;
10
+ /** Override for tests; defaults to the env-driven build. */
11
+ forwarder?: AnalyticsForwarder;
12
+ /** Override for tests; defaults to `process.env`. */
13
+ env?: NodeJS.ProcessEnv;
14
+ }
15
+ export interface AnalyticsModuleHandle {
16
+ ingestService: AnalyticsIngestService;
17
+ queryService: AnalyticsQueryService;
18
+ forwarder: AnalyticsForwarder;
19
+ }
20
+ export declare function analyticsModule(options: AnalyticsModuleOptions): {
21
+ plugin: (app: FastifyInstance) => Promise<void>;
22
+ handle: AnalyticsModuleHandle;
23
+ };
24
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,YAAY,EAAE,mBAAmB,CAAC;IAClC,4DAA4D;IAC5D,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,sBAAsB,CAAC;IACtC,YAAY,EAAE,qBAAqB,CAAC;IACpC,SAAS,EAAE,kBAAkB,CAAC;CAC/B;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG;IAChE,MAAM,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,EAAE,qBAAqB,CAAC;CAC/B,CAgBA"}
@@ -0,0 +1,20 @@
1
+ import { AnalyticsIngestService } from './services/analytics-ingest.service.js';
2
+ import { AnalyticsQueryService } from './services/analytics-query.service.js';
3
+ import { buildForwarderFromEnv, } from './services/ga4-forwarder.js';
4
+ import { registerAnalyticsRoutes } from './routes.js';
5
+ export function analyticsModule(options) {
6
+ const forwarder = options.forwarder ?? buildForwarderFromEnv(options.env ?? process.env);
7
+ const ingestService = new AnalyticsIngestService(options.emFactory, forwarder);
8
+ const queryService = new AnalyticsQueryService(options.emFactory);
9
+ return {
10
+ handle: { ingestService, queryService, forwarder },
11
+ plugin: async (app) => {
12
+ await registerAnalyticsRoutes(app, {
13
+ ingestService,
14
+ queryService,
15
+ requireAdmin: options.requireAdmin,
16
+ });
17
+ },
18
+ };
19
+ }
20
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EACL,qBAAqB,GAEtB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAkBtD,MAAM,UAAU,eAAe,CAAC,OAA+B;IAI7D,MAAM,SAAS,GACb,OAAO,CAAC,SAAS,IAAI,qBAAqB,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC;IACzE,MAAM,aAAa,GAAG,IAAI,sBAAsB,CAAC,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC/E,MAAM,YAAY,GAAG,IAAI,qBAAqB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAElE,OAAO;QACL,MAAM,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE;QAClD,MAAM,EAAE,KAAK,EAAE,GAAoB,EAAE,EAAE;YACrC,MAAM,uBAAuB,CAAC,GAAG,EAAE;gBACjC,aAAa;gBACb,YAAY;gBACZ,YAAY,EAAE,OAAO,CAAC,YAAY;aACnC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { AnalyticsIngestService } from './services/analytics-ingest.service.js';
3
+ import type { AnalyticsQueryService } from './services/analytics-query.service.js';
4
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
5
+ export interface AnalyticsRoutesDeps {
6
+ ingestService: AnalyticsIngestService;
7
+ queryService: AnalyticsQueryService;
8
+ requireAdmin: RequireAdminFactory;
9
+ }
10
+ export declare function registerAnalyticsRoutes(app: FastifyInstance, deps: AnalyticsRoutesDeps): Promise<void>;
11
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK/C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,sBAAsB,CAAC;IACtC,YAAY,EAAE,qBAAqB,CAAC;IACpC,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,IAAI,CAAC,CA6Bf"}
@@ -0,0 +1,22 @@
1
+ import { analyticsSummaryQuerySchema, ingestAnalyticsBatchRequestSchema, } from '@endora-commerce/contracts';
2
+ export async function registerAnalyticsRoutes(app, deps) {
3
+ const { ingestService, queryService, requireAdmin } = deps;
4
+ // Public ingest — accepts batches from storefront + admin. No auth gate by
5
+ // design (FR-110): we want anonymous browsing events, and the rate limiter
6
+ // already protects against floods.
7
+ app.post('/api/v1/analytics/events', { schema: { body: ingestAnalyticsBatchRequestSchema } }, async (request, reply) => {
8
+ const body = ingestAnalyticsBatchRequestSchema.parse(request.body);
9
+ const result = await ingestService.ingest({
10
+ events: body.events,
11
+ requestId: request.id,
12
+ });
13
+ reply.status(202);
14
+ return { data: result };
15
+ });
16
+ app.get('/api/v1/admin/analytics/summary', { preHandler: requireAdmin('analytics:read') }, async (request) => {
17
+ const query = analyticsSummaryQuerySchema.parse(request.query);
18
+ const summary = await queryService.summary(query);
19
+ return { data: summary };
20
+ });
21
+ }
22
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AACA,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,GAClC,MAAM,4BAA4B,CAAC;AAWpC,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,GAAoB,EACpB,IAAyB;IAEzB,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAE3D,2EAA2E;IAC3E,2EAA2E;IAC3E,mCAAmC;IACnC,GAAG,CAAC,IAAI,CACN,0BAA0B,EAC1B,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,iCAAiC,EAAE,EAAE,EACvD,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,iCAAiC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACnE,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC;YACxC,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,SAAS,EAAE,OAAO,CAAC,EAAE;SACtB,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,iCAAiC,EACjC,EAAE,UAAU,EAAE,YAAY,CAAC,gBAAgB,CAAC,EAAE,EAC9C,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,KAAK,GAAG,2BAA2B,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC/D,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAClD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC3B,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type IngestAnalyticsEvent } from '@endora-commerce/contracts';
3
+ import type { AnalyticsForwarder } from './ga4-forwarder.js';
4
+ /**
5
+ * AnalyticsIngestService (T237 / FR-110).
6
+ *
7
+ * Validates each event in the batch independently — a single bad row should
8
+ * not reject the whole batch. Persisted rows are also handed to the optional
9
+ * GA4 forwarder; the forwarder is best-effort and never blocks ingest.
10
+ */
11
+ export interface IngestResult {
12
+ accepted: number;
13
+ rejected: Array<{
14
+ index: number;
15
+ reason: string;
16
+ }>;
17
+ }
18
+ export declare class AnalyticsIngestService {
19
+ private readonly emFactory;
20
+ private readonly forwarder?;
21
+ constructor(emFactory: () => EntityManager, forwarder?: AnalyticsForwarder | undefined);
22
+ ingest(input: {
23
+ events: IngestAnalyticsEvent[];
24
+ requestId?: string;
25
+ }): Promise<IngestResult>;
26
+ }
27
+ //# sourceMappingURL=analytics-ingest.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics-ingest.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/analytics-ingest.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAA8B,KAAK,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAEnG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE7D;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAED,qBAAa,sBAAsB;IAE/B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;gBADV,SAAS,EAAE,MAAM,aAAa,EAC9B,SAAS,CAAC,EAAE,kBAAkB,YAAA;IAG3C,MAAM,CAAC,KAAK,EAAE;QAClB,MAAM,EAAE,oBAAoB,EAAE,CAAC;QAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,YAAY,CAAC;CAqC1B"}