@reactionary/source 0.3.0 → 0.3.2

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 (103) hide show
  1. package/core/src/client/client-builder.ts +3 -7
  2. package/core/src/client/client.ts +2 -3
  3. package/core/src/decorators/reactionary.decorator.ts +2 -2
  4. package/core/src/initialization.ts +11 -3
  5. package/core/src/providers/analytics.provider.ts +75 -0
  6. package/core/src/providers/cart.provider.ts +3 -0
  7. package/core/src/providers/category.provider.ts +1 -0
  8. package/core/src/providers/identity.provider.ts +5 -0
  9. package/core/src/schemas/errors/invalid-input.error.ts +1 -1
  10. package/core/src/schemas/errors/invalid-output.error.ts +1 -1
  11. package/core/src/schemas/models/identifiers.model.ts +3 -0
  12. package/core/src/schemas/models/order.model.ts +2 -2
  13. package/core/src/schemas/mutations/analytics/index.ts +23 -0
  14. package/core/src/schemas/mutations/analytics/product-add-to-cart.mutation.ts +25 -0
  15. package/core/src/schemas/mutations/analytics/product-details-view.mutation.ts +14 -0
  16. package/core/src/schemas/mutations/analytics/product-summary-click.mutation.ts +26 -0
  17. package/core/src/schemas/mutations/analytics/product-summary-view.mutation.ts +25 -0
  18. package/core/src/schemas/mutations/analytics/purchase.mutation.ts +14 -0
  19. package/core/src/schemas/mutations/index.ts +1 -1
  20. package/core/src/schemas/queries/order-search.query.ts +3 -0
  21. package/core/src/schemas/session.schema.ts +21 -9
  22. package/core/src/test/client-builder.spec.ts +60 -0
  23. package/core/src/zod-utils.ts +3 -1
  24. package/documentation/{1-purpose.md → docs/1-purpose.md} +4 -0
  25. package/documentation/docs/8-tracking.md +9 -0
  26. package/documentation/docs/providers/analytics.provider.md +297 -0
  27. package/documentation/docs/providers/base.provider.md +118 -0
  28. package/documentation/docs/providers/cart.provider.md +305 -0
  29. package/documentation/docs/providers/category.provider.md +244 -0
  30. package/documentation/docs/providers/checkout.provider.md +315 -0
  31. package/documentation/docs/providers/identity.provider.md +194 -0
  32. package/documentation/docs/providers/inventory.provider.md +162 -0
  33. package/documentation/docs/providers/order-search.provider.md +155 -0
  34. package/documentation/docs/providers/order.provider.md +160 -0
  35. package/documentation/docs/providers/price.provider.md +197 -0
  36. package/documentation/docs/providers/product-search.provider.md +265 -0
  37. package/documentation/docs/providers/product.provider.md +204 -0
  38. package/documentation/docs/providers/profile.provider.md +283 -0
  39. package/documentation/docs/providers/store.provider.md +146 -0
  40. package/documentation/docs/schemas/schemas.md +1862 -0
  41. package/documentation/docusaurus.config.js +33 -0
  42. package/documentation/scripts/generate.ts +52 -0
  43. package/documentation/sidebars.js +8 -0
  44. package/documentation/src/css/custom.css +3 -0
  45. package/documentation/src/pages/index.js +12 -0
  46. package/eslint.config.mjs +1 -1
  47. package/examples/node/package.json +6 -6
  48. package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +0 -2
  49. package/examples/node/src/basic/client-creation.spec.ts +2 -2
  50. package/package.json +19 -5
  51. package/providers/algolia/README.md +12 -4
  52. package/providers/algolia/project.json +1 -1
  53. package/providers/algolia/src/core/initialize.ts +7 -2
  54. package/providers/algolia/src/providers/analytics.provider.ts +114 -0
  55. package/providers/algolia/src/providers/index.ts +1 -0
  56. package/providers/algolia/src/providers/product-search.provider.ts +5 -4
  57. package/providers/algolia/src/test/analytics.spec.ts +138 -0
  58. package/providers/commercetools/project.json +1 -1
  59. package/providers/commercetools/src/providers/identity.provider.ts +8 -1
  60. package/providers/commercetools/src/providers/profile.provider.ts +1 -4
  61. package/providers/commercetools/src/test/caching.spec.ts +3 -3
  62. package/providers/commercetools/src/test/identity.spec.ts +2 -2
  63. package/providers/fake/project.json +1 -1
  64. package/providers/fake/src/providers/analytics.provider.ts +5 -0
  65. package/providers/fake/src/providers/checkout.provider.ts +5 -2
  66. package/providers/fake/src/providers/product.provider.ts +18 -8
  67. package/providers/fake/src/test/cart.provider.spec.ts +0 -2
  68. package/providers/fake/src/test/category.provider.spec.ts +3 -3
  69. package/providers/fake/src/test/checkout.provider.spec.ts +3 -7
  70. package/providers/google-analytics/README.md +11 -0
  71. package/providers/google-analytics/eslint.config.mjs +25 -0
  72. package/providers/google-analytics/package.json +12 -0
  73. package/providers/google-analytics/project.json +33 -0
  74. package/providers/google-analytics/src/core/initialize.ts +16 -0
  75. package/providers/google-analytics/src/index.ts +4 -0
  76. package/providers/google-analytics/src/providers/analytics.provider.ts +162 -0
  77. package/providers/google-analytics/src/schema/capabilities.schema.ts +10 -0
  78. package/providers/google-analytics/src/schema/configuration.schema.ts +9 -0
  79. package/providers/google-analytics/src/test/analytics.provider.spec.ts +93 -0
  80. package/providers/google-analytics/tsconfig.json +24 -0
  81. package/providers/google-analytics/tsconfig.lib.json +23 -0
  82. package/providers/google-analytics/tsconfig.spec.json +28 -0
  83. package/providers/google-analytics/vite.config.ts +26 -0
  84. package/providers/google-analytics/vitest.config.mts +21 -0
  85. package/providers/medusa/package.json +3 -10
  86. package/providers/medusa/project.json +1 -1
  87. package/providers/medusa/src/providers/identity.provider.ts +34 -10
  88. package/providers/medusa/src/providers/profile.provider.ts +5 -15
  89. package/providers/medusa/src/test/test-utils.ts +0 -1
  90. package/providers/medusa/tsconfig.json +3 -0
  91. package/providers/medusa/tsconfig.lib.json +16 -1
  92. package/providers/meilisearch/project.json +1 -1
  93. package/providers/posthog/project.json +1 -1
  94. package/tsconfig.base.json +4 -1
  95. package/.claude/settings.local.json +0 -28
  96. package/core/src/schemas/mutations/analytics.mutation.ts +0 -23
  97. package/providers/algolia/src/test/test-utils.ts +0 -31
  98. /package/documentation/{2-getting-started.md → docs/2-getting-started.md} +0 -0
  99. /package/documentation/{3-querying-and-changing-data.md → docs/3-querying-and-changing-data.md} +0 -0
  100. /package/documentation/{4-product-data.md → docs/4-product-data.md} +0 -0
  101. /package/documentation/{5-cart-and-checkout.md → docs/5-cart-and-checkout.md} +0 -0
  102. /package/documentation/{6-product-search.md → docs/6-product-search.md} +0 -0
  103. /package/documentation/{7-marketing.md → docs/7-marketing.md} +0 -0
@@ -1,6 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { describe, expect, it } from 'vitest';
3
- import { CommercetoolsClient } from '../core/client.js';
3
+ import { CommercetoolsAPI } from '../core/client.js';
4
4
  import { getCommercetoolsTestConfiguration } from './test-utils.js';
5
5
  import {
6
6
  createInitialRequestContext,
@@ -12,7 +12,7 @@ import type { CommercetoolsConfiguration } from '../schema/configuration.schema.
12
12
  function setup() {
13
13
  const config = getCommercetoolsTestConfiguration();
14
14
  const context = createInitialRequestContext();
15
- const root = new CommercetoolsClient(config, context);
15
+ const root = new CommercetoolsAPI(config, context);
16
16
 
17
17
  return {
18
18
  config,
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "provider-fake",
2
+ "name": "fake",
3
3
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
4
  "sourceRoot": "providers/fake/src",
5
5
  "projectType": "library",
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ AnalyticsMutation,
2
3
  Cache,
3
4
  RequestContext} from '@reactionary/core';
4
5
  import {
@@ -15,4 +16,8 @@ export class FakeAnalyticsProvider extends AnalyticsProvider {
15
16
 
16
17
  this.config = config;
17
18
  }
19
+
20
+ public override async track(event: AnalyticsMutation): Promise<void> {
21
+ // No-op
22
+ }
18
23
  }
@@ -29,9 +29,12 @@ import {
29
29
  CheckoutMutationFinalizeCheckoutSchema,
30
30
  success,
31
31
  type CheckoutIdentifier,
32
+ PaymentMethodSchema,
33
+ ShippingMethodSchema,
32
34
  } from '@reactionary/core';
33
35
  import type { FakeConfiguration } from '../schema/configuration.schema.js';
34
36
  import { base, en, Faker } from '@faker-js/faker';
37
+ import z from 'zod';
35
38
 
36
39
  export class FakeCheckoutProvider extends CheckoutProvider {
37
40
  protected config: FakeConfiguration;
@@ -90,7 +93,7 @@ export class FakeCheckoutProvider extends CheckoutProvider {
90
93
 
91
94
  @Reactionary({
92
95
  inputSchema: CheckoutQueryForAvailableShippingMethodsSchema,
93
- outputSchema: CheckoutSchema,
96
+ outputSchema: z.array(ShippingMethodSchema),
94
97
  })
95
98
  public override async getAvailableShippingMethods(
96
99
  payload: CheckoutQueryForAvailableShippingMethods
@@ -116,7 +119,7 @@ export class FakeCheckoutProvider extends CheckoutProvider {
116
119
 
117
120
  @Reactionary({
118
121
  inputSchema: CheckoutQueryForAvailablePaymentMethodsSchema,
119
- outputSchema: CheckoutSchema,
122
+ outputSchema: z.array(PaymentMethodSchema),
120
123
  })
121
124
  public override async getAvailablePaymentMethods(
122
125
  payload: CheckoutQueryForAvailablePaymentMethods
@@ -14,7 +14,7 @@ import {
14
14
  type Result,
15
15
  error,
16
16
  success,
17
- type NotFoundError
17
+ type NotFoundError,
18
18
  } from '@reactionary/core';
19
19
  import type z from 'zod';
20
20
  import type { FakeConfiguration } from '../schema/configuration.schema.js';
@@ -23,7 +23,11 @@ import { base, en, Faker } from '@faker-js/faker';
23
23
  export class FakeProductProvider extends ProductProvider {
24
24
  protected config: FakeConfiguration;
25
25
 
26
- constructor(config: FakeConfiguration, cache: ReactinaryCache, context: RequestContext) {
26
+ constructor(
27
+ config: FakeConfiguration,
28
+ cache: ReactinaryCache,
29
+ context: RequestContext
30
+ ) {
27
31
  super(cache, context);
28
32
 
29
33
  this.config = config;
@@ -31,7 +35,11 @@ export class FakeProductProvider extends ProductProvider {
31
35
 
32
36
  @Reactionary({
33
37
  inputSchema: ProductQueryByIdSchema,
34
- outputSchema: ProductSchema
38
+ outputSchema: ProductSchema,
39
+ cache: true,
40
+ cacheTimeToLiveInSeconds: 300,
41
+ currencyDependentCaching: false,
42
+ localeDependentCaching: true,
35
43
  })
36
44
  public override async getById(
37
45
  payload: ProductQueryById
@@ -41,7 +49,7 @@ export class FakeProductProvider extends ProductProvider {
41
49
 
42
50
  @Reactionary({
43
51
  inputSchema: ProductQueryBySlugSchema,
44
- outputSchema: ProductSchema
52
+ outputSchema: ProductSchema,
45
53
  })
46
54
  public override async getBySlug(
47
55
  payload: ProductQueryBySlug
@@ -53,7 +61,9 @@ export class FakeProductProvider extends ProductProvider {
53
61
  inputSchema: ProductQueryBySKUSchema,
54
62
  outputSchema: ProductSchema,
55
63
  })
56
- public override async getBySKU(payload: ProductQueryBySKU): Promise<Result<Product>> {
64
+ public override async getBySKU(
65
+ payload: ProductQueryBySKU
66
+ ): Promise<Result<Product>> {
57
67
  return success(this.parseSingle(payload.variant.sku));
58
68
  }
59
69
 
@@ -79,12 +89,12 @@ export class FakeProductProvider extends ProductProvider {
79
89
  ean: '',
80
90
  gtin: '',
81
91
  identifier: {
82
- sku: ''
92
+ sku: '',
83
93
  },
84
94
  images: [],
85
95
  name: '',
86
96
  options: [],
87
- upc: ''
97
+ upc: '',
88
98
  },
89
99
  description: generator.commerce.productDescription(),
90
100
  manufacturer: '',
@@ -93,7 +103,7 @@ export class FakeProductProvider extends ProductProvider {
93
103
  published: true,
94
104
  sharedAttributes: [],
95
105
  variants: [],
96
- } satisfies Product
106
+ } satisfies Product;
97
107
 
98
108
  return result;
99
109
  }
@@ -72,8 +72,6 @@ describe('Fake Cart Provider', () => {
72
72
  expect(updatedCart.value.items.length).toBe(1);
73
73
  expect(updatedCart.value.items[0].variant.sku).toBe(testData.skuWithoutTiers);
74
74
  expect(updatedCart.value.items[0].quantity).toBe(3);
75
- expect(updatedCart.value.items[0].price.totalPrice.value).toBe(cart.value.items[0].price.totalPrice.value * 3);
76
- expect(updatedCart.value.items[0].price.unitPrice.value).toBe(cart.value.items[0].price.unitPrice.value);
77
75
  });
78
76
 
79
77
  it('should be able to remove an item from a cart', async () => {
@@ -158,16 +158,16 @@ describe('Faker Category Provider', () => {
158
158
  expect(result.value.text).not.toBe('');
159
159
  });
160
160
 
161
- it('returns a placeholder if you search for a category that does not exist', async () => {
161
+ it('returns a not found error for categories that do not exist', async () => {
162
162
  const result = await provider.getById({
163
163
  id: { key: 'non-existent-category' },
164
164
  });
165
165
 
166
- if (!result.success) {
166
+ if (result.success) {
167
167
  assert.fail();
168
168
  }
169
169
 
170
- expect(result.value.identifier.key).toBe('non-existent-category');
170
+ expect(result.error.type).toBe('NotFound');
171
171
  });
172
172
 
173
173
  describe('caching', () => {
@@ -1,15 +1,11 @@
1
1
  import 'dotenv/config';
2
2
  import type { RequestContext } from '@reactionary/core';
3
3
  import {
4
- CartSchema,
5
- IdentitySchema,
6
4
  NoOpCache,
7
5
  createInitialRequestContext,
8
6
  } from '@reactionary/core';
9
7
  import { getFakerTestConfiguration } from './test-utils.js';
10
- import { FakeCartProvider } from '../providers/cart.provider.js';
11
- import { FakeIdentityProvider } from '../providers/index.js';
12
- import { describe, expect, it, beforeAll, beforeEach, assert } from 'vitest';
8
+ import { describe, expect, it, beforeEach, assert } from 'vitest';
13
9
  import { FakeCheckoutProvider } from '../providers/checkout.provider.js';
14
10
 
15
11
  describe('Fake Checkout Provider', () => {
@@ -200,7 +196,7 @@ describe('Fake Checkout Provider', () => {
200
196
  });
201
197
 
202
198
  if (!result.success) {
203
- assert.fail();
199
+ assert.fail(JSON.stringify(result.error));
204
200
  }
205
201
 
206
202
  expect(result.value.length).toBeGreaterThan(0);
@@ -214,7 +210,7 @@ describe('Fake Checkout Provider', () => {
214
210
  });
215
211
 
216
212
  if (!result.success) {
217
- assert.fail();
213
+ assert.fail(JSON.stringify(result.error));
218
214
  }
219
215
 
220
216
  expect(result.value.length).toBeGreaterThan(0);
@@ -0,0 +1,11 @@
1
+ # google-analytics
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Building
6
+
7
+ Run `nx build google-analytics` to build the library.
8
+
9
+ ## Running unit tests
10
+
11
+ Run `nx test google-analytics` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,25 @@
1
+ import baseConfig from '../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.json'],
7
+ rules: {
8
+ '@nx/dependency-checks': [
9
+ 'error',
10
+ {
11
+ ignoredFiles: [
12
+ '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
13
+ '{projectRoot}/esbuild.config.{js,ts,mjs,mts}',
14
+ '{projectRoot}/vite.config.{js,ts,mjs,mts}',
15
+ '{projectRoot}/**/*.spec.ts',
16
+ ],
17
+ ignoredDependencies: ['vitest', '@nx/vite'],
18
+ },
19
+ ],
20
+ },
21
+ languageOptions: {
22
+ parser: await import('jsonc-eslint-parser'),
23
+ },
24
+ },
25
+ ];
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@reactionary/google-analytics",
3
+ "version": "0.0.1",
4
+ "main": "index.js",
5
+ "types": "src/index.d.ts",
6
+ "dependencies": {
7
+ "@reactionary/core": "0.0.1",
8
+ "zod": "4.1.9"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false
12
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "google-analytics",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "providers/google-analytics/src",
5
+ "projectType": "library",
6
+ "release": {
7
+ "version": {
8
+ "manifestRootsToUpdate": ["dist/{projectRoot}"],
9
+ "currentVersionResolver": "git-tag",
10
+ "fallbackCurrentVersionResolver": "disk"
11
+ }
12
+ },
13
+ "tags": [],
14
+ "targets": {
15
+ "build": {
16
+ "executor": "@nx/esbuild:esbuild",
17
+ "outputs": ["{options.outputPath}"],
18
+ "options": {
19
+ "outputPath": "dist/providers/google-analytics",
20
+ "main": "providers/google-analytics/src/index.ts",
21
+ "tsConfig": "providers/google-analytics/tsconfig.lib.json",
22
+ "assets": ["providers/google-analytics/*.md"],
23
+ "format": ["esm"],
24
+ "bundle": false
25
+ }
26
+ },
27
+ "nx-release-publish": {
28
+ "options": {
29
+ "packageRoot": "dist/{projectRoot}"
30
+ }
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,16 @@
1
+ import type { Cache, ClientFromCapabilities, RequestContext } from "@reactionary/core";
2
+ import type { GoogleAnalyticsCapabilities } from "../schema/capabilities.schema.js";
3
+ import type { GoogleAnalyticsConfiguration } from "../schema/configuration.schema.js";
4
+ import { GoogleAnalyticsAnalyticsProvider } from "../providers/analytics.provider.js";
5
+
6
+ export function googleAnalyticsCapabilities<T extends GoogleAnalyticsCapabilities>(configuration: GoogleAnalyticsConfiguration, capabilities: T) {
7
+ return (cache: Cache, context: RequestContext): ClientFromCapabilities<T> => {
8
+ const client: any = {};
9
+
10
+ if (capabilities.analytics) {
11
+ client.analytics = new GoogleAnalyticsAnalyticsProvider(cache, context, configuration);
12
+ }
13
+
14
+ return client;
15
+ };
16
+ }
@@ -0,0 +1,4 @@
1
+ export * from './core/initialize.js';
2
+ export * from './providers/analytics.provider.js';
3
+ export * from './schema/capabilities.schema.js';
4
+ export * from './schema/configuration.schema.js';
@@ -0,0 +1,162 @@
1
+ import {
2
+ AnalyticsProvider,
3
+ type RequestContext,
4
+ type Cache,
5
+ type AnalyticsMutationProductSummaryViewEvent,
6
+ type AnalyticsMutationProductSummaryClickEvent,
7
+ type AnalyticsMutationProductDetailsViewEvent,
8
+ type AnalyticsMutationProductAddToCartEvent,
9
+ type AnalyticsMutationPurchaseEvent,
10
+ } from '@reactionary/core';
11
+ import type { GoogleAnalyticsConfiguration } from '../schema/configuration.schema.js';
12
+
13
+ export class GoogleAnalyticsAnalyticsProvider extends AnalyticsProvider {
14
+ protected config: GoogleAnalyticsConfiguration;
15
+
16
+ constructor(
17
+ cache: Cache,
18
+ context: RequestContext,
19
+ configuration: GoogleAnalyticsConfiguration
20
+ ) {
21
+ super(cache, context);
22
+
23
+ this.config = configuration;
24
+ }
25
+
26
+ protected override async processProductSummaryView(
27
+ event: AnalyticsMutationProductSummaryViewEvent
28
+ ) {
29
+ const gaEvent = {
30
+ client_id: this.context.session.identityContext.personalizationKey,
31
+ user_id: this.context.session.identityContext.personalizationKey,
32
+ events: [
33
+ {
34
+ name: 'view_item_list',
35
+ params: {
36
+ currency: this.context.languageContext.currencyCode,
37
+ items: event.products.map((x) => {
38
+ return {
39
+ item_id: x.key,
40
+ };
41
+ }),
42
+ },
43
+ },
44
+ ],
45
+ };
46
+
47
+ await this.sendEvent(gaEvent);
48
+ }
49
+
50
+ protected override async processProductSummaryClick(
51
+ event: AnalyticsMutationProductSummaryClickEvent
52
+ ) {
53
+ const gaEvent = {
54
+ client_id: this.context.session.identityContext.personalizationKey,
55
+ user_id: this.context.session.identityContext.personalizationKey,
56
+ events: [
57
+ {
58
+ name: 'select_item',
59
+ params: {
60
+ currency: this.context.languageContext.currencyCode,
61
+ items: [
62
+ {
63
+ item_id: event.product.key,
64
+ index: event.position,
65
+ },
66
+ ],
67
+ },
68
+ },
69
+ ],
70
+ };
71
+
72
+ await this.sendEvent(gaEvent);
73
+ }
74
+
75
+ protected override async processProductDetailsView(
76
+ event: AnalyticsMutationProductDetailsViewEvent
77
+ ) {
78
+ const gaEvent = {
79
+ client_id: this.context.session.identityContext.personalizationKey,
80
+ user_id: this.context.session.identityContext.personalizationKey,
81
+ events: [
82
+ {
83
+ name: 'view_item',
84
+ params: {
85
+ currency: this.context.languageContext.currencyCode,
86
+ items: [
87
+ {
88
+ item_id: event.product.key,
89
+ },
90
+ ],
91
+ },
92
+ },
93
+ ],
94
+ };
95
+
96
+ await this.sendEvent(gaEvent);
97
+ }
98
+
99
+ protected override async processProductAddToCart(
100
+ event: AnalyticsMutationProductAddToCartEvent
101
+ ) {
102
+ const gaEvent = {
103
+ client_id: this.context.session.identityContext.personalizationKey,
104
+ user_id: this.context.session.identityContext.personalizationKey,
105
+ events: [
106
+ {
107
+ name: 'add_to_cart',
108
+ params: {
109
+ currency: this.context.languageContext.currencyCode,
110
+ items: [
111
+ {
112
+ item_id: event.product.key,
113
+ },
114
+ ],
115
+ },
116
+ },
117
+ ],
118
+ };
119
+
120
+ await this.sendEvent(gaEvent);
121
+ }
122
+
123
+ protected override async processPurchase(
124
+ event: AnalyticsMutationPurchaseEvent
125
+ ) {
126
+ const gaEvent = {
127
+ client_id: this.context.session.identityContext.personalizationKey,
128
+ user_id: this.context.session.identityContext.personalizationKey,
129
+ events: [
130
+ {
131
+ name: 'purchase',
132
+ params: {
133
+ currency: this.context.languageContext.currencyCode,
134
+ transaction_id: event.order.identifier.key,
135
+ value: event.order.price.grandTotal.value,
136
+ tax: event.order.price.totalTax.value,
137
+ shipping: event.order.price.totalShipping.value,
138
+ items: event.order.items.map((item) => ({
139
+ item_id: item.variant.sku,
140
+ quantity: item.quantity,
141
+ price: item.price.unitPrice.value,
142
+ })),
143
+ },
144
+ },
145
+ ],
146
+ };
147
+
148
+ await this.sendEvent(gaEvent);
149
+ }
150
+
151
+ protected async sendEvent(event: unknown) {
152
+ const url = `${this.config.url}?measurement_id=${this.config.measurementId}&api_secret=${this.config.apiSecret}`;
153
+
154
+ await fetch(url, {
155
+ method: 'POST',
156
+ headers: {
157
+ 'Content-Type': 'application/json',
158
+ },
159
+ body: JSON.stringify(event),
160
+ });
161
+ }
162
+ }
@@ -0,0 +1,10 @@
1
+ import { CapabilitiesSchema } from '@reactionary/core';
2
+ import type { z } from 'zod';
3
+
4
+ export const GoogleAnalyticsCapabilitiesSchema = CapabilitiesSchema.pick({
5
+ analytics: true,
6
+ }).partial();
7
+
8
+ export type GoogleAnalyticsCapabilities = z.infer<
9
+ typeof GoogleAnalyticsCapabilitiesSchema
10
+ >;
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+
3
+ export const GoogleAnalyticsConfigurationSchema = z.looseObject({
4
+ url: z.string(),
5
+ measurementId: z.string(),
6
+ apiSecret: z.string()
7
+ });
8
+
9
+ export type GoogleAnalyticsConfiguration = z.infer<typeof GoogleAnalyticsConfigurationSchema>;
@@ -0,0 +1,93 @@
1
+ import 'dotenv/config';
2
+ import type { AnalyticsMutationProductSummaryViewEvent, AnalyticsMutationPurchaseEvent, RequestContext } from '@reactionary/core';
3
+ import {
4
+ CartSchema,
5
+ IdentitySchema,
6
+ NoOpCache,
7
+ createInitialRequestContext,
8
+ } from '@reactionary/core';
9
+ import { describe, expect, it, beforeAll, beforeEach, assert } from 'vitest';
10
+ import { GoogleAnalyticsAnalyticsProvider } from '../providers/analytics.provider.js';
11
+ import type { GoogleAnalyticsConfiguration } from '../schema/configuration.schema.js';
12
+
13
+ describe('Google Analytics Analytics Provider', () => {
14
+ let provider: GoogleAnalyticsAnalyticsProvider;
15
+ let reqCtx: RequestContext;
16
+
17
+ beforeEach(() => {
18
+ reqCtx = createInitialRequestContext();
19
+ const config = {
20
+ apiSecret: process.env['GOOGLE_ANALYTICS_API_SECRET'] || '',
21
+ measurementId: process.env['GOOGLE_ANALYTICS_MEASUREMENT_ID'] || '',
22
+ url: process.env['GOOGLE_ANALYTICS_URL'] || '',
23
+ } satisfies GoogleAnalyticsConfiguration;
24
+
25
+ provider = new GoogleAnalyticsAnalyticsProvider(
26
+ new NoOpCache(),
27
+ reqCtx,
28
+ config
29
+ );
30
+ });
31
+
32
+ describe('tracking', () => {
33
+ it('should be able to add an item to a cart', async () => {
34
+ const event = {
35
+ event: 'product-summary-view',
36
+ products: [{
37
+ key: 'P-5000'
38
+ }]
39
+ } satisfies AnalyticsMutationProductSummaryViewEvent;
40
+
41
+ const result = await provider.track(event);
42
+ });
43
+
44
+ it('should be able to track a purchase', async () => {
45
+ const event = {
46
+ event: 'purchase',
47
+ order: {
48
+ identifier: { key: 'ORDER-12345' },
49
+ userId: { userId: 'test-user-123' },
50
+ items: [
51
+ {
52
+ identifier: { key: 'item-1' },
53
+ variant: { sku: 'SKU-001' },
54
+ quantity: 2,
55
+ price: {
56
+ unitPrice: { value: 29.99, currency: 'EUR' },
57
+ unitDiscount: { value: 0, currency: 'EUR' },
58
+ totalPrice: { value: 59.98, currency: 'EUR' },
59
+ totalDiscount: { value: 0, currency: 'EUR' },
60
+ },
61
+ inventoryStatus: 'Allocated',
62
+ },
63
+ {
64
+ identifier: { key: 'item-2' },
65
+ variant: { sku: 'SKU-002' },
66
+ quantity: 1,
67
+ price: {
68
+ unitPrice: { value: 49.99, currency: 'EUR' },
69
+ unitDiscount: { value: 5, currency: 'EUR' },
70
+ totalPrice: { value: 44.99, currency: 'EUR' },
71
+ totalDiscount: { value: 5, currency: 'EUR' },
72
+ },
73
+ inventoryStatus: 'Allocated',
74
+ },
75
+ ],
76
+ price: {
77
+ totalTax: { value: 21.99, currency: 'EUR' },
78
+ totalDiscount: { value: 5, currency: 'EUR' },
79
+ totalSurcharge: { value: 0, currency: 'EUR' },
80
+ totalShipping: { value: 4.99, currency: 'EUR' },
81
+ totalProductPrice: { value: 104.97, currency: 'EUR' },
82
+ grandTotal: { value: 126.95, currency: 'EUR' },
83
+ },
84
+ orderStatus: 'AwaitingPayment',
85
+ inventoryStatus: 'Allocated',
86
+ paymentInstructions: [],
87
+ },
88
+ } satisfies AnalyticsMutationPurchaseEvent;
89
+
90
+ await provider.track(event);
91
+ });
92
+ });
93
+ });
@@ -0,0 +1,24 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "forceConsistentCasingInFileNames": true,
7
+ "strict": true,
8
+ "importHelpers": true,
9
+ "noImplicitOverride": true,
10
+ "noImplicitReturns": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "noPropertyAccessFromIndexSignature": true
13
+ },
14
+ "files": [],
15
+ "include": [],
16
+ "references": [
17
+ {
18
+ "path": "./tsconfig.lib.json"
19
+ },
20
+ {
21
+ "path": "./tsconfig.spec.json"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "declaration": true,
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*.ts"],
9
+ "exclude": [
10
+ "vite.config.ts",
11
+ "vite.config.mts",
12
+ "vitest.config.ts",
13
+ "vitest.config.mts",
14
+ "src/**/*.test.ts",
15
+ "src/**/*.spec.ts",
16
+ "src/**/*.test.tsx",
17
+ "src/**/*.spec.tsx",
18
+ "src/**/*.test.js",
19
+ "src/**/*.spec.js",
20
+ "src/**/*.test.jsx",
21
+ "src/**/*.spec.jsx"
22
+ ]
23
+ }