@endora-commerce/mod-google-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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/admin/api/google-analytics-client.d.ts +17 -0
  4. package/dist/admin/api/google-analytics-client.d.ts.map +1 -0
  5. package/dist/admin/api/google-analytics-client.js +21 -0
  6. package/dist/admin/api/google-analytics-client.js.map +1 -0
  7. package/dist/admin/index.d.ts +29 -0
  8. package/dist/admin/index.d.ts.map +1 -0
  9. package/dist/admin/index.js +63 -0
  10. package/dist/admin/index.js.map +1 -0
  11. package/dist/admin/pages/CustomEventEditPage.d.ts +3 -0
  12. package/dist/admin/pages/CustomEventEditPage.d.ts.map +1 -0
  13. package/dist/admin/pages/CustomEventEditPage.js +179 -0
  14. package/dist/admin/pages/CustomEventEditPage.js.map +1 -0
  15. package/dist/admin/pages/CustomEventsListPage.d.ts +8 -0
  16. package/dist/admin/pages/CustomEventsListPage.d.ts.map +1 -0
  17. package/dist/admin/pages/CustomEventsListPage.js +36 -0
  18. package/dist/admin/pages/CustomEventsListPage.js.map +1 -0
  19. package/dist/backend/entities/ga-custom-event.entity.d.ts +26 -0
  20. package/dist/backend/entities/ga-custom-event.entity.d.ts.map +1 -0
  21. package/dist/backend/entities/ga-custom-event.entity.js +82 -0
  22. package/dist/backend/entities/ga-custom-event.entity.js.map +1 -0
  23. package/dist/backend/index.d.ts +85 -0
  24. package/dist/backend/index.d.ts.map +1 -0
  25. package/dist/backend/index.js +102 -0
  26. package/dist/backend/index.js.map +1 -0
  27. package/dist/backend/routes.admin.d.ts +15 -0
  28. package/dist/backend/routes.admin.d.ts.map +1 -0
  29. package/dist/backend/routes.admin.js +35 -0
  30. package/dist/backend/routes.admin.js.map +1 -0
  31. package/dist/backend/routes.storefront.d.ts +17 -0
  32. package/dist/backend/routes.storefront.d.ts.map +1 -0
  33. package/dist/backend/routes.storefront.js +30 -0
  34. package/dist/backend/routes.storefront.js.map +1 -0
  35. package/dist/backend/services/cookie-consent-block-seeder.d.ts +29 -0
  36. package/dist/backend/services/cookie-consent-block-seeder.d.ts.map +1 -0
  37. package/dist/backend/services/cookie-consent-block-seeder.js +51 -0
  38. package/dist/backend/services/cookie-consent-block-seeder.js.map +1 -0
  39. package/dist/backend/services/custom-events.service.d.ts +54 -0
  40. package/dist/backend/services/custom-events.service.d.ts.map +1 -0
  41. package/dist/backend/services/custom-events.service.js +158 -0
  42. package/dist/backend/services/custom-events.service.js.map +1 -0
  43. package/dist/backend/services/ga-config.service.d.ts +27 -0
  44. package/dist/backend/services/ga-config.service.d.ts.map +1 -0
  45. package/dist/backend/services/ga-config.service.js +66 -0
  46. package/dist/backend/services/ga-config.service.js.map +1 -0
  47. package/dist/backend/services/ga4-mp-client.d.ts +32 -0
  48. package/dist/backend/services/ga4-mp-client.d.ts.map +1 -0
  49. package/dist/backend/services/ga4-mp-client.js +46 -0
  50. package/dist/backend/services/ga4-mp-client.js.map +1 -0
  51. package/dist/backend/services/ss-delivery-queue.d.ts +32 -0
  52. package/dist/backend/services/ss-delivery-queue.d.ts.map +1 -0
  53. package/dist/backend/services/ss-delivery-queue.js +29 -0
  54. package/dist/backend/services/ss-delivery-queue.js.map +1 -0
  55. package/dist/backend/services/ss-delivery.service.d.ts +24 -0
  56. package/dist/backend/services/ss-delivery.service.d.ts.map +1 -0
  57. package/dist/backend/services/ss-delivery.service.js +52 -0
  58. package/dist/backend/services/ss-delivery.service.js.map +1 -0
  59. package/dist/manifest.d.ts +194 -0
  60. package/dist/manifest.d.ts.map +1 -0
  61. package/dist/manifest.js +134 -0
  62. package/dist/manifest.js.map +1 -0
  63. package/dist/migrations/20260715T171116_google_analytics_init.d.ts +14 -0
  64. package/dist/migrations/20260715T171116_google_analytics_init.d.ts.map +1 -0
  65. package/dist/migrations/20260715T171116_google_analytics_init.js +35 -0
  66. package/dist/migrations/20260715T171116_google_analytics_init.js.map +1 -0
  67. package/dist/migrations/index.d.ts +28 -0
  68. package/dist/migrations/index.d.ts.map +1 -0
  69. package/dist/migrations/index.js +28 -0
  70. package/dist/migrations/index.js.map +1 -0
  71. package/docs/google-analytics.md +106 -0
  72. package/i18n/en.json +33 -0
  73. package/i18n/pl.json +33 -0
  74. package/package.json +98 -0
  75. package/tailwind.css +14 -0
@@ -0,0 +1,134 @@
1
+ import { defineModuleManifest, defineModuleSettingsManifest } from '@endora-commerce/contracts';
2
+ import { GOOGLE_ANALYTICS_SETTING_CODES } from '@endora-commerce/contracts';
3
+ /**
4
+ * Google Analytics module (feature 049). Integrates the storefront with
5
+ * Google Analytics 4: per-sales-channel activation + Measurement ID, Enhanced
6
+ * Ecommerce, an optional server-side tagging delivery path (durable BullMQ
7
+ * worker — Principle X), and an admin custom-events builder. Per-channel
8
+ * configuration is stored through the Settings module; the Measurement Protocol
9
+ * API secret uses the `secret` value type (feature 043, AES-256-GCM at rest).
10
+ *
11
+ * Distinct from the legacy `analytics` module (internal first-party event log +
12
+ * env-gated forwarder), which this module supersedes for GA forwarding.
13
+ */
14
+ export const googleAnalyticsSettingsManifest = defineModuleSettingsManifest({
15
+ moduleCode: 'google_analytics',
16
+ groups: [{ code: 'google_analytics', name: 'Google Analytics' }],
17
+ settings: [
18
+ {
19
+ // Feature 073 — the operator's activation control. Platform-wide, and
20
+ // deliberately not `google_analytics.enabled`: that code already exists
21
+ // and is per-sales-channel, answering "does GA4 load on this storefront".
22
+ // This one answers "does this client have Google Analytics at all".
23
+ code: 'google_analytics.module_enabled',
24
+ name: 'Google Analytics module enabled',
25
+ description: 'Switches GA4 injection, the custom-event mappings, the server-side delivery queue and the admin screen on or off for the whole platform. Separate from the per-channel switch, which decides where the tag actually loads. Nothing is dropped: mappings stay in the database and every setting keeps its value.',
26
+ groupCode: 'google_analytics',
27
+ valueType: 'boolean',
28
+ defaultValue: true,
29
+ },
30
+ {
31
+ code: GOOGLE_ANALYTICS_SETTING_CODES.ENABLED,
32
+ name: 'Enable Google Analytics',
33
+ description: 'Master switch for the module. Per-channel overridable.',
34
+ groupCode: 'google_analytics',
35
+ valueType: 'boolean',
36
+ defaultValue: false,
37
+ },
38
+ {
39
+ code: GOOGLE_ANALYTICS_SETTING_CODES.MEASUREMENT_ID,
40
+ name: 'Measurement ID',
41
+ description: 'GA4 Measurement ID (G-XXXXXXXXXX). Blank means the channel is untracked.',
42
+ groupCode: 'google_analytics',
43
+ valueType: 'string',
44
+ defaultValue: '',
45
+ },
46
+ {
47
+ code: GOOGLE_ANALYTICS_SETTING_CODES.ENHANCED_ECOMMERCE_ENABLED,
48
+ name: 'Enhanced Ecommerce',
49
+ description: 'Emit GA4 recommended ecommerce events (view_item, add_to_cart, begin_checkout, purchase).',
50
+ groupCode: 'google_analytics',
51
+ valueType: 'boolean',
52
+ defaultValue: false,
53
+ },
54
+ {
55
+ code: GOOGLE_ANALYTICS_SETTING_CODES.SERVER_SIDE_ENABLED,
56
+ name: 'Server-side tagging',
57
+ description: 'Route the channel\'s events through the server-side delivery worker instead of the browser.',
58
+ groupCode: 'google_analytics',
59
+ valueType: 'boolean',
60
+ defaultValue: false,
61
+ },
62
+ {
63
+ code: GOOGLE_ANALYTICS_SETTING_CODES.SERVER_SIDE_ENDPOINT,
64
+ name: 'Server-side endpoint',
65
+ description: 'Server-side GTM container URL. Blank uses the GA4 Measurement Protocol default endpoint.',
66
+ groupCode: 'google_analytics',
67
+ valueType: 'string',
68
+ defaultValue: '',
69
+ },
70
+ {
71
+ code: GOOGLE_ANALYTICS_SETTING_CODES.SERVER_SIDE_API_SECRET,
72
+ name: 'Measurement Protocol API secret',
73
+ description: 'GA4 Measurement Protocol API secret (GA4 Admin → Data Streams → Measurement Protocol API secrets). Stored encrypted at rest; write-only.',
74
+ groupCode: 'google_analytics',
75
+ valueType: 'secret',
76
+ defaultValue: '',
77
+ },
78
+ {
79
+ code: GOOGLE_ANALYTICS_SETTING_CODES.REQUIRE_CONSENT,
80
+ name: 'Require analytics consent',
81
+ description: 'When enabled, GA loads in Consent Mode v2 denied-by-default until consent is granted.',
82
+ groupCode: 'google_analytics',
83
+ valueType: 'boolean',
84
+ defaultValue: true,
85
+ },
86
+ ],
87
+ });
88
+ export const manifest = defineModuleManifest({
89
+ id: 'google_analytics',
90
+ name: 'Google Analytics',
91
+ description: 'Google Analytics 4 integration: per-sales-channel activation and Measurement ID, Enhanced Ecommerce, configurable custom events, and optional server-side tagging.',
92
+ version: '1.0.0',
93
+ // `auth` owns the `requireAdmin` port the admin routes are gated by; feature
94
+ // 072 made it a container resolution rather than a constructor argument.
95
+ //
96
+ // `cms` owns `cmsBlockSeedPort`, the seam the cookie-consent banner message
97
+ // is kept in place through (feature 075 / D-87). Declared rather than withheld
98
+ // as non-binding because the edge is binding in the direction that matters to
99
+ // an operator: the banner text *is* that block, and a consent banner with no
100
+ // message is not a reduced banner — it is a consent this deployment cannot
101
+ // show it asked for.
102
+ dependencies: ['audit_logs', 'cms', 'sales_channels', 'settings', 'auth'],
103
+ settings: googleAnalyticsSettingsManifest,
104
+ i18n: { bundlesDir: 'i18n' },
105
+ docs: { dir: 'docs' },
106
+ permissions: [
107
+ { code: 'google_analytics:read', label: 'View Google Analytics configuration' },
108
+ { code: 'google_analytics:write', label: 'Manage Google Analytics configuration and custom events' },
109
+ ],
110
+ actions: [
111
+ {
112
+ id: 'open-google-analytics',
113
+ labelKey: 'actions.openGoogleAnalytics.label',
114
+ descriptionKey: 'actions.openGoogleAnalytics.description',
115
+ icon: 'Sparkles',
116
+ targetRoute: '/google-analytics',
117
+ requiredPermission: 'google_analytics:read',
118
+ keywords: ['google', 'analytics', 'ga4', 'tracking', 'events', 'ecommerce'],
119
+ weight: 240,
120
+ },
121
+ {
122
+ id: 'new-google-analytics-event',
123
+ labelKey: 'actions.newCustomEvent.label',
124
+ descriptionKey: 'actions.newCustomEvent.description',
125
+ icon: 'Plus',
126
+ targetRoute: '/google-analytics/new',
127
+ requiredPermission: 'google_analytics:write',
128
+ keywords: ['google', 'analytics', 'ga4', 'event', 'zdarzenie', 'custom'],
129
+ weight: 241,
130
+ },
131
+ ],
132
+ activation: { settingCode: 'google_analytics.module_enabled', default: true },
133
+ });
134
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Feature 049 — Google Analytics.
4
+ *
5
+ * Creates `ga_custom_events`: admin-defined mappings from a storefront trigger
6
+ * action to a named GA4 event, with the selected payload fields stored inline
7
+ * as JSONB (research §R9 / data-model simplification). Per-channel config lives
8
+ * in the Settings module (no table). `sales_channel_id` null ⇒ all channels.
9
+ */
10
+ export declare class Migration20260715T171116GoogleAnalyticsInit extends Migration {
11
+ up(): Promise<void>;
12
+ down(): Promise<void>;
13
+ }
14
+ //# sourceMappingURL=20260715T171116_google_analytics_init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260715T171116_google_analytics_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260715T171116_google_analytics_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;GAOG;AACH,qBAAa,2CAA4C,SAAQ,SAAS;IACzD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrC"}
@@ -0,0 +1,35 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Feature 049 — Google Analytics.
4
+ *
5
+ * Creates `ga_custom_events`: admin-defined mappings from a storefront trigger
6
+ * action to a named GA4 event, with the selected payload fields stored inline
7
+ * as JSONB (research §R9 / data-model simplification). Per-channel config lives
8
+ * in the Settings module (no table). `sales_channel_id` null ⇒ all channels.
9
+ */
10
+ export class Migration20260715T171116GoogleAnalyticsInit extends Migration {
11
+ async up() {
12
+ this.addSql(`
13
+ create table "ga_custom_events" (
14
+ "id" uuid not null,
15
+ "sales_channel_id" uuid null,
16
+ "event_name" varchar(40) not null,
17
+ "trigger_action" varchar(32) not null,
18
+ "button_id" varchar(128) null,
19
+ "enabled" boolean not null default true,
20
+ "fields" jsonb not null default '[]',
21
+ "version" int not null default 1,
22
+ "created_at" timestamptz not null,
23
+ "updated_at" timestamptz not null,
24
+ constraint "ga_custom_events_pkey" primary key ("id")
25
+ );
26
+ `);
27
+ this.addSql(`create index "ga_custom_events_channel_action_idx" on "ga_custom_events" ("sales_channel_id", "trigger_action");`);
28
+ this.addSql(`alter table "ga_custom_events" add constraint "ga_custom_events_sales_channel_id_foreign" ` +
29
+ `foreign key ("sales_channel_id") references "sales_channels" ("id") on update cascade on delete cascade;`);
30
+ }
31
+ async down() {
32
+ this.addSql(`drop table if exists "ga_custom_events" cascade;`);
33
+ }
34
+ }
35
+ //# sourceMappingURL=20260715T171116_google_analytics_init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260715T171116_google_analytics_init.js","sourceRoot":"","sources":["../../src/migrations/20260715T171116_google_analytics_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,OAAO,2CAA4C,SAAQ,SAAS;IAC/D,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;KAcX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,kHAAkH,CACnH,CAAC;QACF,IAAI,CAAC,MAAM,CACT,4FAA4F;YAC1F,0GAA0G,CAC7G,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,kDAAkD,CAAC,CAAC;IAClE,CAAC;CACF"}
@@ -0,0 +1,28 @@
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. There is exactly one class here, which
8
+ * is the case most likely to tempt an author into publishing the class alone and
9
+ * calling it done — `blog` shipped `export *` on that reasoning until D-168, and
10
+ * an installed `blog` stopped the platform from booting.
11
+ *
12
+ * One class means the intra-module order this array carries is trivial, and it
13
+ * still is not the order the migration runs in: where this module's block sits
14
+ * relative to every other module's is decided by the manifest `dependencies`
15
+ * graph (feature 081), and this one names `sales_channels`, whose table the
16
+ * `ga_custom_events` foreign key references.
17
+ *
18
+ * The **named** export stays, and the asymmetry with `./backend` — which
19
+ * publishes an array and no named class (D-168) — is deliberate.
20
+ * `db/migrations-registry.generated.ts` imports the class by name from this
21
+ * specifier, and a migration class name is contract in a way an entity class
22
+ * name is not: `mikro_orm_migrations` persists it, so it is a string every
23
+ * already-migrated database holds.
24
+ */
25
+ import { Migration20260715T171116GoogleAnalyticsInit } from './20260715T171116_google_analytics_init.js';
26
+ export declare const migrations: (typeof Migration20260715T171116GoogleAnalyticsInit)[];
27
+ export { Migration20260715T171116GoogleAnalyticsInit };
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,2CAA2C,EAAE,MAAM,4CAA4C,CAAC;AAEzG,eAAO,MAAM,UAAU,wDAAgD,CAAC;AAExE,OAAO,EAAE,2CAA2C,EAAE,CAAC"}
@@ -0,0 +1,28 @@
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. There is exactly one class here, which
8
+ * is the case most likely to tempt an author into publishing the class alone and
9
+ * calling it done — `blog` shipped `export *` on that reasoning until D-168, and
10
+ * an installed `blog` stopped the platform from booting.
11
+ *
12
+ * One class means the intra-module order this array carries is trivial, and it
13
+ * still is not the order the migration runs in: where this module's block sits
14
+ * relative to every other module's is decided by the manifest `dependencies`
15
+ * graph (feature 081), and this one names `sales_channels`, whose table the
16
+ * `ga_custom_events` foreign key references.
17
+ *
18
+ * The **named** export stays, and the asymmetry with `./backend` — which
19
+ * publishes an array and no named class (D-168) — is deliberate.
20
+ * `db/migrations-registry.generated.ts` imports the class by name from this
21
+ * specifier, and a migration class name is contract in a way an entity class
22
+ * name is not: `mikro_orm_migrations` persists it, so it is a string every
23
+ * already-migrated database holds.
24
+ */
25
+ import { Migration20260715T171116GoogleAnalyticsInit } from './20260715T171116_google_analytics_init.js';
26
+ export const migrations = [Migration20260715T171116GoogleAnalyticsInit];
27
+ export { Migration20260715T171116GoogleAnalyticsInit };
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,2CAA2C,EAAE,MAAM,4CAA4C,CAAC;AAEzG,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,2CAA2C,CAAC,CAAC;AAExE,OAAO,EAAE,2CAA2C,EAAE,CAAC"}
@@ -0,0 +1,106 @@
1
+ ---
2
+ title: Google Analytics
3
+ description: Google Analytics 4 for the storefront — per-channel Measurement ID, Enhanced Ecommerce, a custom-events builder and optional server-side tagging
4
+ ---
5
+
6
+ # Google Analytics
7
+
8
+ The `google_analytics` module integrates the storefront with
9
+ **Google Analytics 4**: per-Sales-Channel activation and Measurement ID,
10
+ Enhanced Ecommerce, an admin-configurable custom-events builder, and an optional
11
+ **server-side tagging** delivery path. It is a separate module from the legacy
12
+ internal `analytics` event log and supersedes that module's env-gated GA4
13
+ forwarder.
14
+
15
+ ## Configuration (Settings module)
16
+
17
+ All per-channel configuration lives in the Settings module under the
18
+ `google_analytics.*` group (global value + per-channel override):
19
+
20
+ | Setting | Type | Meaning |
21
+ | --- | --- | --- |
22
+ | `google_analytics.enabled` | boolean | Master switch (per-channel overridable). |
23
+ | `google_analytics.measurement_id` | string | GA4 `G-XXXXXXXXXX`. Blank ⇒ channel untracked. |
24
+ | `google_analytics.enhanced_ecommerce_enabled` | boolean | Emit GA4 ecommerce events. |
25
+ | `google_analytics.server_side_enabled` | boolean | Route events through the server-side worker. |
26
+ | `google_analytics.server_side_endpoint` | string | Server-side GTM container URL (blank ⇒ GA4 Measurement Protocol). |
27
+ | `google_analytics.server_side_api_secret` | secret | Measurement Protocol API secret (needs `SETTINGS_SECRET_ENCRYPTION_KEY`). |
28
+ | `google_analytics.require_consent` | boolean | Load GA in Consent Mode v2 denied-by-default until consent is granted. |
29
+
30
+ A channel with a blank Measurement ID emits nothing, regardless of the master
31
+ switch.
32
+
33
+ ## Storefront behaviour
34
+
35
+ - `gtag.js` is injected with `next/script` (`afterInteractive`, Core-Web-Vitals
36
+ safe) and initialized with `send_page_view: false`.
37
+ - A `page_view` is emitted on every App-Router navigation (initial load plus
38
+ each client-side route change) — exactly one per destination.
39
+ - **Consent Mode v2**: when `require_consent` is on, GA starts in the denied
40
+ default. A cookie banner drives `updateAnalyticsConsent(granted)` to flip to
41
+ granted. (This module provides the consent plumbing, not a full cookie
42
+ banner.)
43
+ - **Enhanced Ecommerce** (when enabled): `view_item` (product page),
44
+ `add_to_cart`, `begin_checkout` (checkout), and `purchase` (order-confirmation
45
+ page).
46
+
47
+ ## Custom events
48
+
49
+ Admins define custom events on the `/google-analytics` admin screen. Each event
50
+ has a name (GA4 event-name shape), a trigger action, and a selected subset of
51
+ the fields available for that action:
52
+
53
+ | Trigger action | Available fields |
54
+ | --- | --- |
55
+ | `contact_form_submitted` | all contact-form fields **except file uploads** |
56
+ | `place_order_clicked` | the checkout submission fields |
57
+ | `add_to_cart` / `add_to_quote_request` / `add_to_shopping_list` | `sku`, `name`, `price`, `quantity` |
58
+ | `button_click_by_id` | the originating page + the button's `data-*` attributes (requires a button ID) |
59
+
60
+ Only the selected fields are sent; absent fields are omitted (never blocking the
61
+ event). File uploads are never included. Custom events may be scoped to one
62
+ channel or all channels, and multiple events may bind to the same action.
63
+
64
+ > The storefront has no contact form yet; the `contact_form_submitted` trigger
65
+ > ships as a client hook (`trackContactFormSubmit`) that becomes active once a
66
+ > contact form is added.
67
+
68
+ ## Server-side tagging (pure Measurement Protocol)
69
+
70
+ When `server_side_enabled` is on for a channel the module runs **pure
71
+ server-side tagging through the platform's own server** — no external container
72
+ and no browser-side Google library:
73
+
74
+ - **gtag.js is not loaded.** Every event (page_view, Enhanced Ecommerce, custom)
75
+ is posted to `POST /api/v1/storefront/google-analytics/collect`, so **no hit
76
+ reaches Google directly from the browser** (best ad-blocker resilience, no
77
+ client/server double count).
78
+ - The route is a **pure producer** — it validates and enqueues one job per event
79
+ onto the durable BullMQ queue `google_analytics.ss.deliver`. A separable worker
80
+ (co-located in the API process unless `BACKEND_ROLE=api`, then only in the
81
+ `worker` process) forwards each event to GA4 via the **Measurement Protocol**,
82
+ retrying on failure. Each event carries a stable `eventId` idempotency key.
83
+ - **GA4 default metrics come for free.** The browser owns a first-party
84
+ `client_id` and a rolling 30-minute `session_id` (cookies only once consent is
85
+ granted; ephemeral in-memory before that) and sends `engagement_time_msec` with
86
+ every event, so GA4 auto-derives `first_visit`, `session_start`, sessions, and
87
+ active users from the MP stream.
88
+ - **Optional destination override.** By default the worker sends to GA4's
89
+ Measurement Protocol endpoint. Set `server_side_endpoint` to route events to a
90
+ different collection URL instead (e.g. a self-hosted proxy or a server-side GTM
91
+ container). The `server_side_api_secret` (a GA4 Measurement Protocol API
92
+ secret, created under GA4 Admin → Data Streams → Measurement Protocol API
93
+ secrets) authenticates delivery.
94
+ - **Consent** is carried in the MP payload (`analyticsStorage`) reflecting the
95
+ visitor's actual banner decision.
96
+ - **Enhanced Measurement** is replicated in the browser (since gtag.js is not
97
+ loaded) and routed through the server: `scroll` (90%), outbound `click`,
98
+ `file_download`, `form_start` / `form_submit`, and `view_search_results` (site
99
+ search). Video engagement is not covered — it would require a client-side
100
+ YouTube-player integration. In client mode gtag.js emits all of these itself.
101
+
102
+ ## Permissions
103
+
104
+ `google_analytics:read` and `google_analytics:write` gate the admin surface
105
+ (custom-events CRUD). Per-channel Settings are managed through the generic
106
+ Settings admin screen.
package/i18n/en.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "actions.openGoogleAnalytics.label": "Google Analytics",
3
+ "actions.openGoogleAnalytics.description": "Configure Google Analytics custom events",
4
+ "actions.newCustomEvent.label": "New Google Analytics event",
5
+ "actions.newCustomEvent.description": "Create a custom Google Analytics event",
6
+ "nav.googleAnalytics.label": "Google Analytics",
7
+ "customEvents.title": "Custom events",
8
+ "customEvents.subtitle": "Map storefront actions to Google Analytics events",
9
+ "customEvents.empty": "No custom events yet",
10
+ "customEvents.new": "New event",
11
+ "customEvents.edit": "Edit event",
12
+ "customEvents.eventName": "Event name",
13
+ "customEvents.triggerAction": "Trigger action",
14
+ "customEvents.buttonId": "Button ID",
15
+ "customEvents.salesChannel": "Sales channel",
16
+ "customEvents.allChannels": "All channels",
17
+ "customEvents.enabled": "Enabled",
18
+ "customEvents.fields": "Payload fields",
19
+ "customEvents.fieldsHint": "Pick which fields to send — values are filled automatically at the trigger.",
20
+ "customEvents.addField": "Add field",
21
+ "customEvents.fieldKey": "Field",
22
+ "customEvents.payloadKey": "Sent as",
23
+ "customEvents.sentAs": "sent as",
24
+ "customEvents.payloadKeyHint": "(same as field)",
25
+ "customEvents.save": "Save",
26
+ "customEvents.delete": "Delete",
27
+ "customEvents.actions.contact_form_submitted": "Contact form submitted",
28
+ "customEvents.actions.place_order_clicked": "Place Order clicked",
29
+ "customEvents.actions.add_to_cart": "Add to cart",
30
+ "customEvents.actions.add_to_quote_request": "Add to quote request",
31
+ "customEvents.actions.add_to_shopping_list": "Add to shopping list",
32
+ "customEvents.actions.button_click_by_id": "Button click (by ID)"
33
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "actions.openGoogleAnalytics.label": "Google Analytics",
3
+ "actions.openGoogleAnalytics.description": "Konfiguruj własne zdarzenia Google Analytics",
4
+ "actions.newCustomEvent.label": "Nowe zdarzenie Google Analytics",
5
+ "actions.newCustomEvent.description": "Utwórz własne zdarzenie Google Analytics",
6
+ "nav.googleAnalytics.label": "Google Analytics",
7
+ "customEvents.title": "Zdarzenia własne",
8
+ "customEvents.subtitle": "Przypisz akcje w Storefront do zdarzeń Google Analytics",
9
+ "customEvents.empty": "Brak zdarzeń własnych",
10
+ "customEvents.new": "Nowe zdarzenie",
11
+ "customEvents.edit": "Edytuj zdarzenie",
12
+ "customEvents.eventName": "Nazwa zdarzenia",
13
+ "customEvents.triggerAction": "Akcja wyzwalająca",
14
+ "customEvents.buttonId": "ID przycisku",
15
+ "customEvents.salesChannel": "Kanał sprzedaży",
16
+ "customEvents.allChannels": "Wszystkie kanały",
17
+ "customEvents.enabled": "Aktywne",
18
+ "customEvents.fields": "Pola payloadu",
19
+ "customEvents.fieldsHint": "Wybierz, które pola wysłać — wartości uzupełniają się automatycznie w miejscu zdarzenia.",
20
+ "customEvents.addField": "Dodaj pole",
21
+ "customEvents.fieldKey": "Pole",
22
+ "customEvents.payloadKey": "Wysyłane jako",
23
+ "customEvents.sentAs": "wysyłane jako",
24
+ "customEvents.payloadKeyHint": "(jak nazwa pola)",
25
+ "customEvents.save": "Zapisz",
26
+ "customEvents.delete": "Usuń",
27
+ "customEvents.actions.contact_form_submitted": "Zapis formularza kontaktowego",
28
+ "customEvents.actions.place_order_clicked": "Kliknięcie „Złóż zamówienie”",
29
+ "customEvents.actions.add_to_cart": "Dodaj do koszyka",
30
+ "customEvents.actions.add_to_quote_request": "Dodaj do zapytania ofertowego",
31
+ "customEvents.actions.add_to_shopping_list": "Dodaj do listy zakupowej",
32
+ "customEvents.actions.button_click_by_id": "Kliknięcie przycisku (wg ID)"
33
+ }
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "@endora-commerce/mod-google-analytics",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Google Analytics 4 integration: per-sales-channel activation and Measurement ID, Enhanced Ecommerce, configurable custom events, and optional server-side tagging.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "google_analytics"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/google_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
+ "bullmq": "^5",
54
+ "fastify": "^5",
55
+ "ioredis": "^5",
56
+ "react": "^19",
57
+ "react-router-dom": "^7",
58
+ "zod": "^4",
59
+ "@endora-commerce/admin-kit": "0.100.0",
60
+ "@endora-commerce/contracts": "0.100.0",
61
+ "@endora-commerce/platform": "0.100.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@endora-commerce/admin-kit": {
65
+ "optional": true
66
+ },
67
+ "react": {
68
+ "optional": true
69
+ },
70
+ "react-router-dom": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "devDependencies": {
75
+ "@mikro-orm/core": "^6.6.13",
76
+ "@mikro-orm/migrations": "^6.6.13",
77
+ "@mikro-orm/postgresql": "^6.6.13",
78
+ "@types/node": "^22.9.0",
79
+ "@types/react": "^19.2.14",
80
+ "bullmq": "^5.76.1",
81
+ "fastify": "^5.12.5",
82
+ "ioredis": "^5.10.1",
83
+ "react": "^19.2.5",
84
+ "react-router-dom": "^7.18.2",
85
+ "typescript": "^5.9.3",
86
+ "vitest": "^4.1.11",
87
+ "zod": "^4.2.0",
88
+ "@endora-commerce/admin-kit": "0.100.0",
89
+ "@endora-commerce/contracts": "0.100.0",
90
+ "@endora-commerce/platform": "0.100.0"
91
+ },
92
+ "scripts": {
93
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.json",
94
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.ui.json --noEmit",
95
+ "lint": "eslint src",
96
+ "test": "vitest run"
97
+ }
98
+ }
package/tailwind.css ADDED
@@ -0,0 +1,14 @@
1
+ /* @endora-commerce/mod-google-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";