@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
@@ -0,0 +1,37 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * The two tenant-key indexes on `analytics_events` (feature 050, research
4
+ * §R5 — the guard filters on `organization_id` and `customer_account_id`,
5
+ * and neither column was indexed).
6
+ *
7
+ * They were created by the platform's frozen
8
+ * `Migration20260717T134752CoreTenantScopeIndexes` until
9
+ * `specs/120-migration-closure-bridge-ownership/` Phase 3. Under D-226 a
10
+ * migration may name a table only if its own module, its transitive
11
+ * `dependencies` closure or the platform creates it; the platform declares
12
+ * no dependencies, so it could never name `analytics_events`. An instance
13
+ * that omits `analytics` had a frozen corpus indexing a table nothing
14
+ * builds. The table is this module's own, so here the closure is trivial.
15
+ *
16
+ * The statements are the frozen ones verbatim, `if not exists` and all, so
17
+ * the two paths reach one schema. A database that has already applied the
18
+ * frozen migration is offered nothing from it — the storage keys on the
19
+ * class name and holds no checksum — and these two are the no-ops they
20
+ * already read as.
21
+ *
22
+ * The frozen migration's note is worth carrying: `analytics_events` can grow
23
+ * large, and a plain in-transaction `CREATE INDEX` locks writes while it
24
+ * builds. Ops may pre-create both CONCURRENTLY out of band, which
25
+ * `if not exists` makes free.
26
+ */
27
+ export class Migration20260912T125655AnalyticsEventsTenantScopeIndexes extends Migration {
28
+ async up() {
29
+ this.addSql('create index if not exists "analytics_events_organization_id_index" on "analytics_events" ("organization_id");');
30
+ this.addSql('create index if not exists "analytics_events_customer_account_id_index" on "analytics_events" ("customer_account_id");');
31
+ }
32
+ async down() {
33
+ this.addSql('drop index if exists "analytics_events_organization_id_index";');
34
+ this.addSql('drop index if exists "analytics_events_customer_account_id_index";');
35
+ }
36
+ }
37
+ //# sourceMappingURL=20260912T125655_analytics_events_tenant_scope_indexes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260912T125655_analytics_events_tenant_scope_indexes.js","sourceRoot":"","sources":["../../src/migrations/20260912T125655_analytics_events_tenant_scope_indexes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,OAAO,yDAA0D,SAAQ,SAAS;IAC7E,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CACT,gHAAgH,CACjH,CAAC;QACF,IAAI,CAAC,MAAM,CACT,wHAAwH,CACzH,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,gEAAgE,CAAC,CAAC;QAC9E,IAAI,CAAC,MAAM,CAAC,oEAAoE,CAAC,CAAC;IACpF,CAAC;CACF"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's. The first is
12
+ * stamped inside the frozen historical prefix (`BASELINE_THROUGH`,
13
+ * `db/migration-order.ts`), so its position is history and the manifest graph
14
+ * does not move it.
15
+ *
16
+ * The **named** export stays beside the array, and the asymmetry with
17
+ * `./backend` — which publishes an array and no named class (D-168) — is
18
+ * deliberate. `db/migrations-registry.generated.ts` imports the class by name
19
+ * from this specifier, and a migration class name is contract in a way an
20
+ * entity class name is not: `mikro_orm_migrations` persists it, so it is a
21
+ * string every already-migrated database holds.
22
+ */
23
+ import { Migration20260425T143139AnalyticsInit } from './20260425T143139_analytics_init.js';
24
+ import { Migration20260912T125655AnalyticsEventsTenantScopeIndexes } from './20260912T125655_analytics_events_tenant_scope_indexes.js';
25
+ export declare const migrations: (typeof Migration20260425T143139AnalyticsInit)[];
26
+ export { Migration20260425T143139AnalyticsInit, Migration20260912T125655AnalyticsEventsTenantScopeIndexes, };
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,qCAAqC,EAAE,MAAM,qCAAqC,CAAC;AAC5F,OAAO,EAAE,yDAAyD,EAAE,MAAM,4DAA4D,CAAC;AAEvI,eAAO,MAAM,UAAU,kDAGtB,CAAC;AAEF,OAAO,EACL,qCAAqC,EACrC,yDAAyD,GAC1D,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's. The first is
12
+ * stamped inside the frozen historical prefix (`BASELINE_THROUGH`,
13
+ * `db/migration-order.ts`), so its position is history and the manifest graph
14
+ * does not move it.
15
+ *
16
+ * The **named** export stays beside the array, and the asymmetry with
17
+ * `./backend` — which publishes an array and no named class (D-168) — is
18
+ * deliberate. `db/migrations-registry.generated.ts` imports the class by name
19
+ * from this specifier, and a migration class name is contract in a way an
20
+ * entity class name is not: `mikro_orm_migrations` persists it, so it is a
21
+ * string every already-migrated database holds.
22
+ */
23
+ import { Migration20260425T143139AnalyticsInit } from './20260425T143139_analytics_init.js';
24
+ import { Migration20260912T125655AnalyticsEventsTenantScopeIndexes } from './20260912T125655_analytics_events_tenant_scope_indexes.js';
25
+ export const migrations = [
26
+ Migration20260425T143139AnalyticsInit,
27
+ Migration20260912T125655AnalyticsEventsTenantScopeIndexes,
28
+ ];
29
+ export { Migration20260425T143139AnalyticsInit, Migration20260912T125655AnalyticsEventsTenantScopeIndexes, };
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,qCAAqC,EAAE,MAAM,qCAAqC,CAAC;AAC5F,OAAO,EAAE,yDAAyD,EAAE,MAAM,4DAA4D,CAAC;AAEvI,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,qCAAqC;IACrC,yDAAyD;CAC1D,CAAC;AAEF,OAAO,EACL,qCAAqC,EACrC,yDAAyD,GAC1D,CAAC"}
@@ -0,0 +1,74 @@
1
+ ---
2
+ title: analytics
3
+ description: Storefront event ingest + admin aggregation + optional GA4 forwarder
4
+ ---
5
+
6
+ # `analytics`
7
+
8
+ Append-only event log fed by the storefront and admin, plus a small
9
+ aggregation read path for the admin dashboard. An optional GA4 forwarder
10
+ mirrors each ingested event into Google Analytics when configured.
11
+
12
+ ## Public surface
13
+
14
+ | Verb + Path | Audience | Purpose |
15
+ | --- | --- | --- |
16
+ | `POST /api/v1/analytics/events` | storefront / admin (no auth) | Ingest a batch of events (≤ 100) |
17
+ | `GET /api/v1/admin/analytics/summary` | admin (`analytics:read`) | Totals by type + per-day breakdown for a window |
18
+
19
+ ## Event types
20
+
21
+ The Zod boundary accepts a fixed set:
22
+
23
+ - `product.viewed`, `category.viewed`
24
+ - `product.added_to_cart`, `cart.abandoned`
25
+ - `order.placed`
26
+ - `search.performed`, `filter.clicked`
27
+
28
+ Adding a new type is a single edit in
29
+ `packages/contracts/src/analytics.ts`.
30
+
31
+ ## Ingest semantics
32
+
33
+ Storefront sends batches; the boundary Zod schema rejects an entire batch
34
+ if any event is malformed (storefront should not be allowed to drift the
35
+ type union). Per-event validation inside the service still runs in case a
36
+ property field is unexpected — those individual events are reported in the
37
+ response's `rejected` array and the rest of the batch lands.
38
+
39
+ The endpoint returns `202 Accepted` with `{ accepted, rejected[] }`. Each
40
+ stored row mirrors the request's `X-Request-Id` for cross-log correlation.
41
+
42
+ ## Aggregation
43
+
44
+ `AnalyticsQueryService.summary({ from, to, salesChannelId? })` runs two
45
+ window-bounded queries (totals by type, daily totals by type) against the
46
+ `(occurred_at)` and `(type, occurred_at)` indexes. The dashboard never
47
+ queries an unbounded range.
48
+
49
+ ## GA4 forwarder
50
+
51
+ `buildForwarderFromEnv(env)` returns:
52
+
53
+ - `Ga4Forwarder` — when both `ANALYTICS_GA4_MEASUREMENT_ID` and
54
+ `ANALYTICS_GA4_API_SECRET` are set. Each ingest is fire-and-forget POSTed
55
+ to GA4's Measurement Protocol; client_id is bucketed by sessionId →
56
+ customerAccountId → organizationId → `'anonymous'` so GA4 sees coherent
57
+ user journeys.
58
+ - `NoopForwarder` — otherwise. Ingest never blocks on a forwarder failure
59
+ and the source events are durable in `analytics_events` regardless.
60
+
61
+ ## Entities
62
+
63
+ `AnalyticsEvent` — `type`, `occurredAt`, `recordedAt`, optional
64
+ `salesChannelId`, `customerAccountId`, `organizationId`, `sessionId`,
65
+ `properties` (JSONB), `requestId`.
66
+
67
+ ## Extension points
68
+
69
+ - **New consumers** — implement the `AnalyticsForwarder` interface and
70
+ register in the composition root (e.g. PostHog, Mixpanel, internal data
71
+ warehouse).
72
+ - **Pre-aggregated rollups** — the dashboard query is fine at hundreds of
73
+ thousands of events; for sustained million-event volumes, materialise a
74
+ daily rollup table here and have the indexer fold inserts into it.
package/i18n/en.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "actions.openAnalytics.label": "Analytics",
3
+ "actions.openAnalytics.description": "Aggregated counts of storefront events",
4
+ "nav.analytics.label": "Analytics",
5
+ "page.title": "Analytics",
6
+ "page.descriptionPrefix": "Aggregated counts of storefront events. Window:",
7
+ "range.7days": "Last 7 days",
8
+ "range.30days": "Last 30 days",
9
+ "range.90days": "Last 90 days",
10
+ "refresh": "Refresh",
11
+ "loading": "Loading…",
12
+ "empty": "No events recorded in this window.",
13
+ "error.load": "Failed to load summary.",
14
+ "totals.title": "Totals by event type",
15
+ "daily.title": "Daily breakdown",
16
+ "column.eventType": "Event type",
17
+ "column.count": "Count",
18
+ "column.day": "Day"
19
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "actions.openAnalytics.label": "Analityka",
3
+ "actions.openAnalytics.description": "Zagregowane liczniki zdarzeń sklepu",
4
+ "nav.analytics.label": "Analityka",
5
+ "page.title": "Analityka",
6
+ "page.descriptionPrefix": "Zagregowane liczniki zdarzeń sklepu. Okno:",
7
+ "range.7days": "Ostatnie 7 dni",
8
+ "range.30days": "Ostatnie 30 dni",
9
+ "range.90days": "Ostatnie 90 dni",
10
+ "refresh": "Odśwież",
11
+ "loading": "Ładowanie…",
12
+ "empty": "Brak zdarzeń zarejestrowanych w tym oknie.",
13
+ "error.load": "Nie udało się załadować podsumowania.",
14
+ "totals.title": "Sumy wg typu zdarzenia",
15
+ "daily.title": "Rozbicie dzienne",
16
+ "column.eventType": "Typ zdarzenia",
17
+ "column.count": "Liczba",
18
+ "column.day": "Dzień"
19
+ }
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@endora-commerce/mod-analytics",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Server-side analytics aggregation and dashboard data feeds.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "analytics"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/analytics"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./migrations": {
30
+ "types": "./dist/migrations/index.d.ts",
31
+ "default": "./dist/migrations/index.js"
32
+ },
33
+ "./admin": {
34
+ "types": "./dist/admin/index.d.ts",
35
+ "default": "./dist/admin/index.js"
36
+ },
37
+ "./tailwind.css": "./tailwind.css",
38
+ "./package.json": "./package.json"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "i18n",
43
+ "docs",
44
+ "tailwind.css"
45
+ ],
46
+ "engines": {
47
+ "node": ">=22.18.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@mikro-orm/core": "^6",
51
+ "@mikro-orm/migrations": "^6",
52
+ "@mikro-orm/postgresql": "^6",
53
+ "fastify": "^5",
54
+ "lucide-react": "^1",
55
+ "react": "^19",
56
+ "@endora-commerce/admin-kit": "0.100.0",
57
+ "@endora-commerce/contracts": "0.100.0",
58
+ "@endora-commerce/platform": "0.100.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@endora-commerce/admin-kit": {
62
+ "optional": true
63
+ },
64
+ "lucide-react": {
65
+ "optional": true
66
+ },
67
+ "react": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "devDependencies": {
72
+ "@mikro-orm/core": "^6.6.13",
73
+ "@mikro-orm/migrations": "^6.6.13",
74
+ "@mikro-orm/postgresql": "^6.6.13",
75
+ "@types/node": "^22.9.0",
76
+ "@types/react": "^19.2.14",
77
+ "fastify": "^5.12.5",
78
+ "lucide-react": "^1.11.0",
79
+ "react": "^19.2.5",
80
+ "typescript": "^5.9.3",
81
+ "vitest": "^4.1.11",
82
+ "@endora-commerce/admin-kit": "0.100.0",
83
+ "@endora-commerce/platform": "0.100.0",
84
+ "@endora-commerce/contracts": "0.100.0"
85
+ },
86
+ "scripts": {
87
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.json",
88
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.ui.json --noEmit",
89
+ "lint": "eslint src",
90
+ "test": "vitest run"
91
+ }
92
+ }
package/tailwind.css ADDED
@@ -0,0 +1,14 @@
1
+ /* @endora-commerce/mod-analytics — AUTO-GENERATED by `pnpm --filter backend run manifests:generate`.
2
+ *
3
+ * The `@source` directives this package asks its host to scan
4
+ * (`specs/110-instance-repository/contracts/admin-stylesheet-composition.md` R1).
5
+ * They resolve relative to **this file**, so they hold wherever the package is
6
+ * installed — a workspace link here, `node_modules` in a client's instance.
7
+ *
8
+ * The `dist` line is what a published tarball ships and is what an instance
9
+ * scans; the `src` line is inert there and is what keeps `pnpm --filter admin
10
+ * run dev` reading source in this repository. Do not edit: run
11
+ * `pnpm --filter backend run manifests:generate`.
12
+ */
13
+ @source "./dist/admin";
14
+ @source "./src/admin";