@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,56 @@
1
+ import { ingestAnalyticsEventSchema } from '@endora-commerce/contracts';
2
+ import { AnalyticsEvent } from '../entities/analytics-event.entity.js';
3
+ export class AnalyticsIngestService {
4
+ emFactory;
5
+ forwarder;
6
+ constructor(emFactory, forwarder) {
7
+ this.emFactory = emFactory;
8
+ this.forwarder = forwarder;
9
+ }
10
+ async ingest(input) {
11
+ // command-coverage-ignore: telemetry/analytics ingestion — high-volume event
12
+ // recording, not an audited domain-state mutation.
13
+ const em = this.emFactory();
14
+ const accepted = [];
15
+ const rejected = [];
16
+ input.events.forEach((raw, index) => {
17
+ const parsed = ingestAnalyticsEventSchema.safeParse(raw);
18
+ if (!parsed.success) {
19
+ rejected.push({ index, reason: parsed.error.issues[0]?.message ?? 'invalid event' });
20
+ return;
21
+ }
22
+ const ev = parsed.data;
23
+ const row = em.create(AnalyticsEvent, {
24
+ type: ev.type,
25
+ ...(ev.occurredAt ? { occurredAt: new Date(ev.occurredAt) } : { occurredAt: new Date() }),
26
+ ...(ev.salesChannelId !== undefined ? { salesChannelId: ev.salesChannelId } : {}),
27
+ ...(ev.customerAccountId !== undefined ? { customerAccountId: ev.customerAccountId } : {}),
28
+ ...(ev.organizationId !== undefined ? { organizationId: ev.organizationId } : {}),
29
+ ...(ev.sessionId !== undefined ? { sessionId: ev.sessionId } : {}),
30
+ ...(ev.properties !== undefined ? { properties: ev.properties } : {}),
31
+ ...(input.requestId !== undefined ? { requestId: input.requestId } : {}),
32
+ });
33
+ accepted.push(row);
34
+ });
35
+ if (accepted.length > 0) {
36
+ await em.persistAndFlush(accepted);
37
+ if (this.forwarder?.enabled) {
38
+ // Fire-and-forget; never block ingest on the external transport.
39
+ void this.forwarder.forwardMany(accepted.map(toForwardPayload));
40
+ }
41
+ }
42
+ return { accepted: accepted.length, rejected };
43
+ }
44
+ }
45
+ function toForwardPayload(row) {
46
+ return {
47
+ type: row.type,
48
+ occurredAt: row.occurredAt.toISOString(),
49
+ ...(row.salesChannelId ? { salesChannelId: row.salesChannelId } : {}),
50
+ ...(row.customerAccountId ? { customerAccountId: row.customerAccountId } : {}),
51
+ ...(row.organizationId ? { organizationId: row.organizationId } : {}),
52
+ ...(row.sessionId ? { sessionId: row.sessionId } : {}),
53
+ ...(row.properties ? { properties: row.properties } : {}),
54
+ };
55
+ }
56
+ //# sourceMappingURL=analytics-ingest.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics-ingest.service.js","sourceRoot":"","sources":["../../../src/backend/services/analytics-ingest.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAA6B,MAAM,4BAA4B,CAAC;AACnG,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAevE,MAAM,OAAO,sBAAsB;IAEd;IACA;IAFnB,YACmB,SAA8B,EAC9B,SAA8B;QAD9B,cAAS,GAAT,SAAS,CAAqB;QAC9B,cAAS,GAAT,SAAS,CAAqB;IAC9C,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAC,KAGZ;QACC,6EAA6E;QAC7E,mDAAmD;QACnD,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAqB,EAAE,CAAC;QACtC,MAAM,QAAQ,GAA6B,EAAE,CAAC;QAE9C,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YAClC,MAAM,MAAM,GAAG,0BAA0B,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACzD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,eAAe,EAAE,CAAC,CAAC;gBACrF,OAAO;YACT,CAAC;YACD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC;YACvB,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE;gBACpC,IAAI,EAAE,EAAE,CAAC,IAAI;gBACb,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;gBACzF,GAAG,CAAC,EAAE,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjF,GAAG,CAAC,EAAE,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,EAAE,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1F,GAAG,CAAC,EAAE,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjF,GAAG,CAAC,EAAE,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClE,GAAG,CAAC,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrE,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACzE,CAAC,CAAC;YACH,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;QAEH,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,EAAE,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;YACnC,IAAI,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC;gBAC5B,iEAAiE;gBACjE,KAAK,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC;YAClE,CAAC;QACH,CAAC;QAED,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;IACjD,CAAC;CACF;AAED,SAAS,gBAAgB,CAAC,GAAmB;IAS3C,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,UAAU,EAAE,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE;QACxC,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9E,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { AnalyticsSummaryQuery, AnalyticsSummaryResponse } from '@endora-commerce/contracts';
3
+ /**
4
+ * AnalyticsQueryService (T237 / FR-111).
5
+ *
6
+ * Two reads serve the admin dashboard:
7
+ * - `totalsByType` — events per type in the requested window
8
+ * - `daily` — per-day, per-type counts for a stacked-area chart
9
+ *
10
+ * Both are bounded by the time window the caller passes; the dashboard
11
+ * never asks for "all time" so the queries always hit the
12
+ * `(occurred_at)` / `(type, occurred_at)` indexes.
13
+ */
14
+ export declare class AnalyticsQueryService {
15
+ private readonly emFactory;
16
+ constructor(emFactory: () => EntityManager);
17
+ summary(query: AnalyticsSummaryQuery): Promise<AnalyticsSummaryResponse>;
18
+ }
19
+ //# sourceMappingURL=analytics-query.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics-query.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/analytics-query.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAEV,qBAAqB,EACrB,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,qBAAa,qBAAqB;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,OAAO,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,wBAAwB,CAAC;CA6C/E"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * AnalyticsQueryService (T237 / FR-111).
3
+ *
4
+ * Two reads serve the admin dashboard:
5
+ * - `totalsByType` — events per type in the requested window
6
+ * - `daily` — per-day, per-type counts for a stacked-area chart
7
+ *
8
+ * Both are bounded by the time window the caller passes; the dashboard
9
+ * never asks for "all time" so the queries always hit the
10
+ * `(occurred_at)` / `(type, occurred_at)` indexes.
11
+ */
12
+ export class AnalyticsQueryService {
13
+ emFactory;
14
+ constructor(emFactory) {
15
+ this.emFactory = emFactory;
16
+ }
17
+ async summary(query) {
18
+ const em = this.emFactory();
19
+ const conn = em.getConnection();
20
+ const params = [query.from, query.to];
21
+ let channelClause = '';
22
+ if (query.salesChannelId) {
23
+ channelClause = ' and sales_channel_id = ?';
24
+ params.push(query.salesChannelId);
25
+ }
26
+ const totalsRows = await conn.execute(`select type, count(*)::text as count
27
+ from analytics_events
28
+ where occurred_at >= ? and occurred_at < ?${channelClause}
29
+ group by type
30
+ order by type asc`, params);
31
+ const dailyRows = await conn.execute(`select to_char(date_trunc('day', occurred_at), 'YYYY-MM-DD') as day,
32
+ type,
33
+ count(*)::text as count
34
+ from analytics_events
35
+ where occurred_at >= ? and occurred_at < ?${channelClause}
36
+ group by 1, 2
37
+ order by 1 asc, 2 asc`, params);
38
+ return {
39
+ totalsByType: totalsRows.map((r) => ({
40
+ type: r.type,
41
+ count: Number(r.count),
42
+ })),
43
+ daily: dailyRows.map((r) => ({
44
+ day: r.day,
45
+ type: r.type,
46
+ count: Number(r.count),
47
+ })),
48
+ };
49
+ }
50
+ }
51
+ //# sourceMappingURL=analytics-query.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics-query.service.js","sourceRoot":"","sources":["../../../src/backend/services/analytics-query.service.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;GAUG;AACH,MAAM,OAAO,qBAAqB;IACH;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,OAAO,CAAC,KAA4B;QACxC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC;QAEhC,MAAM,MAAM,GAA8B,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QACjE,IAAI,aAAa,GAAG,EAAE,CAAC;QACvB,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;YACzB,aAAa,GAAG,2BAA2B,CAAC;YAC5C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QACpC,CAAC;QAED,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,OAAO,CACnC;;oDAE8C,aAAa;;0BAEvC,EACpB,MAAM,CACP,CAAC;QAEF,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,OAAO,CAGlC;;;;oDAI8C,aAAa;;8BAEnC,EACxB,MAAM,CACP,CAAC;QAEF,OAAO;YACL,YAAY,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACnC,IAAI,EAAE,CAAC,CAAC,IAA0B;gBAClC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;aACvB,CAAC,CAAC;YACH,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC3B,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,IAAI,EAAE,CAAC,CAAC,IAA0B;gBAClC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;aACvB,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * GA4 Measurement Protocol forwarder (T237 / FR-111).
3
+ *
4
+ * Off by default. Activate by setting both env vars:
5
+ * ANALYTICS_GA4_MEASUREMENT_ID
6
+ * ANALYTICS_GA4_API_SECRET
7
+ *
8
+ * The forwarder is intentionally fire-and-forget so that ingest never
9
+ * blocks on the external HTTP. A failed request is logged via the supplied
10
+ * `onError` callback; we do not retry — the source events are already
11
+ * durable in `analytics_events` and an admin replay path can drain them
12
+ * separately if real-time GA4 fidelity is required.
13
+ */
14
+ export interface ForwardableEvent {
15
+ type: string;
16
+ occurredAt: string;
17
+ salesChannelId?: string;
18
+ customerAccountId?: string;
19
+ organizationId?: string;
20
+ sessionId?: string;
21
+ properties?: Record<string, unknown>;
22
+ }
23
+ export interface AnalyticsForwarder {
24
+ readonly enabled: boolean;
25
+ forwardMany(events: ForwardableEvent[]): Promise<void>;
26
+ }
27
+ export interface Ga4ForwarderOptions {
28
+ measurementId: string;
29
+ apiSecret: string;
30
+ /** Optional `fetch` override — used by the integration test. */
31
+ fetchFn?: typeof fetch;
32
+ onError?: (err: unknown) => void;
33
+ }
34
+ export declare class Ga4Forwarder implements AnalyticsForwarder {
35
+ private readonly options;
36
+ readonly enabled = true;
37
+ constructor(options: Ga4ForwarderOptions);
38
+ forwardMany(events: ForwardableEvent[]): Promise<void>;
39
+ }
40
+ /**
41
+ * Off-mode forwarder — used when env vars are missing. Conforms to the same
42
+ * interface so the ingest service can hold a single non-null reference.
43
+ */
44
+ export declare class NoopForwarder implements AnalyticsForwarder {
45
+ readonly enabled = false;
46
+ forwardMany(): Promise<void>;
47
+ }
48
+ export declare function buildForwarderFromEnv(env: NodeJS.ProcessEnv): AnalyticsForwarder;
49
+ //# sourceMappingURL=ga4-forwarder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ga4-forwarder.d.ts","sourceRoot":"","sources":["../../../src/backend/services/ga4-forwarder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClC;AAED,qBAAa,YAAa,YAAW,kBAAkB;IAGzC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,QAAQ,CAAC,OAAO,QAAQ;gBAEK,OAAO,EAAE,mBAAmB;IAEnD,WAAW,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAsC7D;AAED;;;GAGG;AACH,qBAAa,aAAc,YAAW,kBAAkB;IACtD,QAAQ,CAAC,OAAO,SAAS;IACnB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;CAGnC;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,kBAAkB,CAKhF"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * GA4 Measurement Protocol forwarder (T237 / FR-111).
3
+ *
4
+ * Off by default. Activate by setting both env vars:
5
+ * ANALYTICS_GA4_MEASUREMENT_ID
6
+ * ANALYTICS_GA4_API_SECRET
7
+ *
8
+ * The forwarder is intentionally fire-and-forget so that ingest never
9
+ * blocks on the external HTTP. A failed request is logged via the supplied
10
+ * `onError` callback; we do not retry — the source events are already
11
+ * durable in `analytics_events` and an admin replay path can drain them
12
+ * separately if real-time GA4 fidelity is required.
13
+ */
14
+ export class Ga4Forwarder {
15
+ options;
16
+ enabled = true;
17
+ constructor(options) {
18
+ this.options = options;
19
+ }
20
+ async forwardMany(events) {
21
+ if (events.length === 0)
22
+ return;
23
+ const fetchFn = this.options.fetchFn ?? globalThis.fetch;
24
+ const url = `https://www.google-analytics.com/mp/collect` +
25
+ `?measurement_id=${encodeURIComponent(this.options.measurementId)}` +
26
+ `&api_secret=${encodeURIComponent(this.options.apiSecret)}`;
27
+ // GA4's MP requires a non-empty client_id per request and a max of 25
28
+ // events per request. We batch by sessionId or customerAccountId; events
29
+ // with neither use a stable "anonymous" bucket per occurredAt date.
30
+ const batches = bucketByClient(events);
31
+ for (const [clientId, bucket] of batches) {
32
+ for (let i = 0; i < bucket.length; i += 25) {
33
+ const slice = bucket.slice(i, i + 25);
34
+ try {
35
+ await fetchFn(url, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({
39
+ client_id: clientId,
40
+ events: slice.map((ev) => ({
41
+ name: ev.type.replace(/[.-]/g, '_'),
42
+ params: {
43
+ ...ev.properties,
44
+ occurred_at: ev.occurredAt,
45
+ ...(ev.organizationId ? { organization_id: ev.organizationId } : {}),
46
+ ...(ev.salesChannelId ? { sales_channel_id: ev.salesChannelId } : {}),
47
+ },
48
+ })),
49
+ }),
50
+ });
51
+ }
52
+ catch (err) {
53
+ this.options.onError?.(err);
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ /**
60
+ * Off-mode forwarder — used when env vars are missing. Conforms to the same
61
+ * interface so the ingest service can hold a single non-null reference.
62
+ */
63
+ export class NoopForwarder {
64
+ enabled = false;
65
+ async forwardMany() {
66
+ // Intentionally empty.
67
+ }
68
+ }
69
+ export function buildForwarderFromEnv(env) {
70
+ const id = env['ANALYTICS_GA4_MEASUREMENT_ID'];
71
+ const secret = env['ANALYTICS_GA4_API_SECRET'];
72
+ if (!id || !secret)
73
+ return new NoopForwarder();
74
+ return new Ga4Forwarder({ measurementId: id, apiSecret: secret });
75
+ }
76
+ function bucketByClient(events) {
77
+ const out = new Map();
78
+ for (const ev of events) {
79
+ const key = ev.sessionId ?? ev.customerAccountId ?? ev.organizationId ?? 'anonymous';
80
+ const arr = out.get(key) ?? [];
81
+ arr.push(ev);
82
+ out.set(key, arr);
83
+ }
84
+ return out;
85
+ }
86
+ //# sourceMappingURL=ga4-forwarder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ga4-forwarder.js","sourceRoot":"","sources":["../../../src/backend/services/ga4-forwarder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Analytics module — manifest backfill (Module Lifecycle, feature 018).
3
+ *
4
+ * Predates the lifecycle system; this manifest is the static record
5
+ * required so the module participates in the registry. No install /
6
+ * uninstall hook today — the module's schema is owned by earlier
7
+ * platform-wide migrations.
8
+ */
9
+ export declare const manifest: {
10
+ id: string;
11
+ name: string;
12
+ version: string;
13
+ dependencies: string[];
14
+ description?: string | undefined;
15
+ acknowledgedDependencies?: {
16
+ moduleId: string;
17
+ port: string;
18
+ reason: string;
19
+ }[] | undefined;
20
+ nonBindingDependencies?: {
21
+ moduleId: string;
22
+ name: string;
23
+ kind: "contributes-to" | "degrades-without" | "refuses-without";
24
+ reason: string;
25
+ whenAbsent?: string | undefined;
26
+ }[] | undefined;
27
+ activation?: {
28
+ settingCode: string;
29
+ default: boolean;
30
+ } | {
31
+ nonDeactivatable: true;
32
+ reason: string;
33
+ } | undefined;
34
+ settings?: {
35
+ moduleCode: string;
36
+ groups: {
37
+ code: string;
38
+ name: string;
39
+ salesChannelCodes?: string[] | undefined;
40
+ isSystemProtected?: boolean | undefined;
41
+ }[];
42
+ settings: {
43
+ code: string;
44
+ name: string;
45
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
46
+ defaultValue: unknown;
47
+ description?: string | undefined;
48
+ groupCode?: string | undefined;
49
+ previousDefaultValues?: unknown[] | undefined;
50
+ salesChannelCodes?: string[] | undefined;
51
+ enumOptions?: string[] | undefined;
52
+ configurationType?: string | undefined;
53
+ hidden?: boolean | undefined;
54
+ }[];
55
+ } | undefined;
56
+ i18n?: {
57
+ bundlesDir: string;
58
+ } | undefined;
59
+ docs?: false | {
60
+ dir: string;
61
+ } | undefined;
62
+ demo?: false | {
63
+ summary: string;
64
+ seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
65
+ reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
66
+ after?: readonly string[] | undefined;
67
+ package?: string | undefined;
68
+ } | undefined;
69
+ actions?: {
70
+ id: string;
71
+ labelKey: string;
72
+ icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
73
+ targetRoute: string;
74
+ keywords: string[];
75
+ weight: number;
76
+ descriptionKey?: string | undefined;
77
+ requiredPermission?: string | undefined;
78
+ }[] | undefined;
79
+ permissions?: {
80
+ code: string;
81
+ label: string;
82
+ module?: string | undefined;
83
+ description?: string | undefined;
84
+ requires?: string[] | undefined;
85
+ }[] | undefined;
86
+ transactionalEmails?: {
87
+ code: string;
88
+ name: string;
89
+ variables: {
90
+ key: string;
91
+ label: string;
92
+ sampleValue?: string | undefined;
93
+ description?: string | undefined;
94
+ }[];
95
+ description?: string | undefined;
96
+ group?: string | undefined;
97
+ }[] | undefined;
98
+ capabilities?: string[] | undefined;
99
+ exclusiveCapabilities?: {
100
+ key: string;
101
+ errorCode: string;
102
+ }[] | undefined;
103
+ errorCodes?: {
104
+ code: string;
105
+ tokens?: string[] | undefined;
106
+ }[] | undefined;
107
+ blocks?: {
108
+ name: string;
109
+ labelKey: string;
110
+ category: string;
111
+ contexts: ("invoice" | "email" | "cms" | "newsletter")[];
112
+ fields: Record<string, {
113
+ type: "number" | "object" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "uuid" | "richtext";
114
+ label?: string | undefined;
115
+ required?: boolean | undefined;
116
+ options?: {
117
+ label: string;
118
+ value: string | number;
119
+ }[] | undefined;
120
+ refKind?: string | undefined;
121
+ }>;
122
+ descriptionKey?: string | undefined;
123
+ defaultProps?: Record<string, unknown> | undefined;
124
+ responsiveFields?: string[] | undefined;
125
+ previewIcon?: string | undefined;
126
+ weight?: number | undefined;
127
+ }[] | undefined;
128
+ blockCategories?: {
129
+ key: string;
130
+ titleKey: string;
131
+ contexts: ("invoice" | "email" | "cms" | "newsletter")[];
132
+ weight?: number | undefined;
133
+ visible?: boolean | undefined;
134
+ }[] | undefined;
135
+ env?: {
136
+ name: string;
137
+ describes: {
138
+ en: string;
139
+ pl: string;
140
+ };
141
+ requirement: {
142
+ kind: "required";
143
+ } | {
144
+ kind: "requiredWhen";
145
+ input: string;
146
+ equals: string;
147
+ } | {
148
+ kind: "optional";
149
+ without: {
150
+ en: string;
151
+ pl: string;
152
+ };
153
+ };
154
+ secret: boolean;
155
+ generable: boolean;
156
+ owner: {
157
+ kind: "platform";
158
+ } | {
159
+ kind: "application";
160
+ application: "admin" | "backend" | "storefront";
161
+ } | {
162
+ kind: "module";
163
+ moduleId: string;
164
+ };
165
+ consumers: ("admin" | "backend" | "storefront")[];
166
+ addressOf: "admin" | "backend" | "storefront" | null;
167
+ }[] | undefined;
168
+ };
169
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDnB,CAAC"}
@@ -0,0 +1,64 @@
1
+ import { defineModuleManifest } from '@endora-commerce/contracts';
2
+ /**
3
+ * Analytics module — manifest backfill (Module Lifecycle, feature 018).
4
+ *
5
+ * Predates the lifecycle system; this manifest is the static record
6
+ * required so the module participates in the registry. No install /
7
+ * uninstall hook today — the module's schema is owned by earlier
8
+ * platform-wide migrations.
9
+ */
10
+ export const manifest = defineModuleManifest({
11
+ id: 'analytics',
12
+ name: 'Analytics',
13
+ description: 'Server-side analytics aggregation and dashboard data feeds.',
14
+ version: '1.0.0',
15
+ // Feature 072 (wave 1) — the admin summary route is gated by `requireAdmin`,
16
+ // which `auth` owns. With `auth` absent the route cannot be guarded at all,
17
+ // so this is a hard edge rather than a degraded mode.
18
+ dependencies: ['auth'],
19
+ permissions: [{ code: 'analytics:read', label: 'View analytics dashboards' }],
20
+ // Feature 091, Phase 4 — the module owns its admin surface, so it owns the
21
+ // strings that surface is labelled with. `nav.analytics.label` replaces the
22
+ // shared bundle's `appShell.nav.analytics`, and the dashboard's own copy
23
+ // moved here verbatim out of `_i18n`'s `analytics.*` block.
24
+ i18n: { bundlesDir: 'i18n' },
25
+ docs: { dir: 'docs' },
26
+ // Principle XVI. The module has had an admin screen and a sidebar entry since
27
+ // feature 018 and has never been reachable under ⌘K — sidebar-only, which the
28
+ // principle says is not enough. The gap is repaired in the merge request that
29
+ // makes the surface the module's, because the palette entry is the one half
30
+ // of that surface no admin-side test can answer for: it is resolved by the
31
+ // server from this declaration against the effective enabled-set, which is
32
+ // what makes the off-state proof in
33
+ // `backend/test/integration/analytics/off-state.test.ts` possible at all.
34
+ actions: [
35
+ {
36
+ id: 'open-analytics',
37
+ labelKey: 'actions.openAnalytics.label',
38
+ descriptionKey: 'actions.openAnalytics.description',
39
+ icon: 'LineChart',
40
+ targetRoute: '/analytics',
41
+ requiredPermission: 'analytics:read',
42
+ keywords: ['analytics', 'analityka', 'dashboard', 'events', 'zdarzenia', 'raport'],
43
+ weight: 230,
44
+ },
45
+ ],
46
+ settings: {
47
+ moduleCode: 'analytics',
48
+ groups: [{ code: 'analytics', name: 'Analytics' }],
49
+ settings: [
50
+ {
51
+ // Feature 073 — the operator's activation control. Platform-wide,
52
+ // never channel-scoped: activation is not a per-channel question.
53
+ code: 'analytics.enabled',
54
+ name: 'Analytics enabled',
55
+ description: 'Switches server-side analytics on or off. While off nothing is ingested, the dashboards and their API are gone, and no event is forwarded to GA4. Nothing is deleted — the events already collected are preserved and reappear when you switch it back on.',
56
+ groupCode: 'analytics',
57
+ valueType: 'boolean',
58
+ defaultValue: true,
59
+ },
60
+ ],
61
+ },
62
+ activation: { settingCode: 'analytics.enabled', default: true },
63
+ });
64
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,WAAW;IACf,IAAI,EAAE,WAAW;IACjB,WAAW,EACT,6DAA6D;IAC/D,OAAO,EAAE,OAAO;IAChB,6EAA6E;IAC7E,4EAA4E;IAC5E,sDAAsD;IACtD,YAAY,EAAE,CAAC,MAAM,CAAC;IACtB,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;IAC7E,2EAA2E;IAC3E,4EAA4E;IAC5E,yEAAyE;IACzE,4DAA4D;IAC5D,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB,8EAA8E;IAC9E,8EAA8E;IAC9E,8EAA8E;IAC9E,4EAA4E;IAC5E,2EAA2E;IAC3E,2EAA2E;IAC3E,oCAAoC;IACpC,0EAA0E;IAC1E,OAAO,EAAE;QACP;YACE,EAAE,EAAE,gBAAgB;YACpB,QAAQ,EAAE,6BAA6B;YACvC,cAAc,EAAE,mCAAmC;YACnD,IAAI,EAAE,WAAW;YACjB,WAAW,EAAE,YAAY;YACzB,kBAAkB,EAAE,gBAAgB;YACpC,QAAQ,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAC;YAClF,MAAM,EAAE,GAAG;SACZ;KACF;IACD,QAAQ,EAAE;QACR,UAAU,EAAE,WAAW;QACvB,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QAClD,QAAQ,EAAE;YACR;gBACE,kEAAkE;gBAClE,kEAAkE;gBAClE,IAAI,EAAE,mBAAmB;gBACzB,IAAI,EAAE,mBAAmB;gBACzB,WAAW,EACT,4PAA4P;gBAC9P,SAAS,EAAE,WAAW;gBACtB,SAAS,EAAE,SAAS;gBACpB,YAAY,EAAE,IAAI;aACnB;SACF;KACF;IACD,UAAU,EAAE,EAAE,WAAW,EAAE,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE;CAChE,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Analytics module init (T237 / FR-110, FR-111). Single append-only event
4
+ * table. Indexes are tuned for the dashboard's two read patterns:
5
+ *
6
+ * - "show me totals by type for the last 30 days"
7
+ * - "show me a daily breakdown of `product.viewed` for sales channel X"
8
+ */
9
+ export declare class Migration20260425T143139AnalyticsInit extends Migration {
10
+ up(): Promise<void>;
11
+ down(): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=20260425T143139_analytics_init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260425T143139_analytics_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260425T143139_analytics_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;GAMG;AACH,qBAAa,qCAAsC,SAAQ,SAAS;IACnD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrC"}
@@ -0,0 +1,34 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Analytics module init (T237 / FR-110, FR-111). Single append-only event
4
+ * table. Indexes are tuned for the dashboard's two read patterns:
5
+ *
6
+ * - "show me totals by type for the last 30 days"
7
+ * - "show me a daily breakdown of `product.viewed` for sales channel X"
8
+ */
9
+ export class Migration20260425T143139AnalyticsInit extends Migration {
10
+ async up() {
11
+ this.addSql(`
12
+ create table "analytics_events" (
13
+ "id" uuid not null,
14
+ "type" varchar(64) not null,
15
+ "occurred_at" timestamptz not null,
16
+ "recorded_at" timestamptz not null,
17
+ "sales_channel_id" uuid null,
18
+ "customer_account_id" uuid null,
19
+ "organization_id" uuid null,
20
+ "session_id" varchar(64) null,
21
+ "properties" jsonb null,
22
+ "request_id" varchar(64) null,
23
+ constraint "analytics_events_pkey" primary key ("id")
24
+ );
25
+ `);
26
+ this.addSql('create index "analytics_events_occurred_at_index" on "analytics_events" ("occurred_at");');
27
+ this.addSql('create index "analytics_events_sales_channel_id_index" on "analytics_events" ("sales_channel_id");');
28
+ this.addSql('create index "idx_analytics_events_type_occurred_at" on "analytics_events" ("type", "occurred_at");');
29
+ }
30
+ async down() {
31
+ this.addSql('drop table if exists "analytics_events" cascade;');
32
+ }
33
+ }
34
+ //# sourceMappingURL=20260425T143139_analytics_init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260425T143139_analytics_init.js","sourceRoot":"","sources":["../../src/migrations/20260425T143139_analytics_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,OAAO,qCAAsC,SAAQ,SAAS;IACzD,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;KAcX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,0FAA0F,CAAC,CAAC;QACxG,IAAI,CAAC,MAAM,CAAC,oGAAoG,CAAC,CAAC;QAClH,IAAI,CAAC,MAAM,CAAC,qGAAqG,CAAC,CAAC;IACrH,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,kDAAkD,CAAC,CAAC;IAClE,CAAC;CACF"}
@@ -0,0 +1,31 @@
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 declare class Migration20260912T125655AnalyticsEventsTenantScopeIndexes extends Migration {
28
+ up(): Promise<void>;
29
+ down(): Promise<void>;
30
+ }
31
+ //# sourceMappingURL=20260912T125655_analytics_events_tenant_scope_indexes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260912T125655_analytics_events_tenant_scope_indexes.d.ts","sourceRoot":"","sources":["../../src/migrations/20260912T125655_analytics_events_tenant_scope_indexes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,yDAA0D,SAAQ,SAAS;IACvE,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IASnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAIrC"}