@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.
- package/core/src/client/client-builder.ts +3 -7
- package/core/src/client/client.ts +2 -3
- package/core/src/decorators/reactionary.decorator.ts +2 -2
- package/core/src/initialization.ts +11 -3
- package/core/src/providers/analytics.provider.ts +75 -0
- package/core/src/providers/cart.provider.ts +3 -0
- package/core/src/providers/category.provider.ts +1 -0
- package/core/src/providers/identity.provider.ts +5 -0
- package/core/src/schemas/errors/invalid-input.error.ts +1 -1
- package/core/src/schemas/errors/invalid-output.error.ts +1 -1
- package/core/src/schemas/models/identifiers.model.ts +3 -0
- package/core/src/schemas/models/order.model.ts +2 -2
- package/core/src/schemas/mutations/analytics/index.ts +23 -0
- package/core/src/schemas/mutations/analytics/product-add-to-cart.mutation.ts +25 -0
- package/core/src/schemas/mutations/analytics/product-details-view.mutation.ts +14 -0
- package/core/src/schemas/mutations/analytics/product-summary-click.mutation.ts +26 -0
- package/core/src/schemas/mutations/analytics/product-summary-view.mutation.ts +25 -0
- package/core/src/schemas/mutations/analytics/purchase.mutation.ts +14 -0
- package/core/src/schemas/mutations/index.ts +1 -1
- package/core/src/schemas/queries/order-search.query.ts +3 -0
- package/core/src/schemas/session.schema.ts +21 -9
- package/core/src/test/client-builder.spec.ts +60 -0
- package/core/src/zod-utils.ts +3 -1
- package/documentation/{1-purpose.md → docs/1-purpose.md} +4 -0
- package/documentation/docs/8-tracking.md +9 -0
- package/documentation/docs/providers/analytics.provider.md +297 -0
- package/documentation/docs/providers/base.provider.md +118 -0
- package/documentation/docs/providers/cart.provider.md +305 -0
- package/documentation/docs/providers/category.provider.md +244 -0
- package/documentation/docs/providers/checkout.provider.md +315 -0
- package/documentation/docs/providers/identity.provider.md +194 -0
- package/documentation/docs/providers/inventory.provider.md +162 -0
- package/documentation/docs/providers/order-search.provider.md +155 -0
- package/documentation/docs/providers/order.provider.md +160 -0
- package/documentation/docs/providers/price.provider.md +197 -0
- package/documentation/docs/providers/product-search.provider.md +265 -0
- package/documentation/docs/providers/product.provider.md +204 -0
- package/documentation/docs/providers/profile.provider.md +283 -0
- package/documentation/docs/providers/store.provider.md +146 -0
- package/documentation/docs/schemas/schemas.md +1862 -0
- package/documentation/docusaurus.config.js +33 -0
- package/documentation/scripts/generate.ts +52 -0
- package/documentation/sidebars.js +8 -0
- package/documentation/src/css/custom.css +3 -0
- package/documentation/src/pages/index.js +12 -0
- package/eslint.config.mjs +1 -1
- package/examples/node/package.json +6 -6
- package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +0 -2
- package/examples/node/src/basic/client-creation.spec.ts +2 -2
- package/package.json +19 -5
- package/providers/algolia/README.md +12 -4
- package/providers/algolia/project.json +1 -1
- package/providers/algolia/src/core/initialize.ts +7 -2
- package/providers/algolia/src/providers/analytics.provider.ts +114 -0
- package/providers/algolia/src/providers/index.ts +1 -0
- package/providers/algolia/src/providers/product-search.provider.ts +5 -4
- package/providers/algolia/src/test/analytics.spec.ts +138 -0
- package/providers/commercetools/project.json +1 -1
- package/providers/commercetools/src/providers/identity.provider.ts +8 -1
- package/providers/commercetools/src/providers/profile.provider.ts +1 -4
- package/providers/commercetools/src/test/caching.spec.ts +3 -3
- package/providers/commercetools/src/test/identity.spec.ts +2 -2
- package/providers/fake/project.json +1 -1
- package/providers/fake/src/providers/analytics.provider.ts +5 -0
- package/providers/fake/src/providers/checkout.provider.ts +5 -2
- package/providers/fake/src/providers/product.provider.ts +18 -8
- package/providers/fake/src/test/cart.provider.spec.ts +0 -2
- package/providers/fake/src/test/category.provider.spec.ts +3 -3
- package/providers/fake/src/test/checkout.provider.spec.ts +3 -7
- package/providers/google-analytics/README.md +11 -0
- package/providers/google-analytics/eslint.config.mjs +25 -0
- package/providers/google-analytics/package.json +12 -0
- package/providers/google-analytics/project.json +33 -0
- package/providers/google-analytics/src/core/initialize.ts +16 -0
- package/providers/google-analytics/src/index.ts +4 -0
- package/providers/google-analytics/src/providers/analytics.provider.ts +162 -0
- package/providers/google-analytics/src/schema/capabilities.schema.ts +10 -0
- package/providers/google-analytics/src/schema/configuration.schema.ts +9 -0
- package/providers/google-analytics/src/test/analytics.provider.spec.ts +93 -0
- package/providers/google-analytics/tsconfig.json +24 -0
- package/providers/google-analytics/tsconfig.lib.json +23 -0
- package/providers/google-analytics/tsconfig.spec.json +28 -0
- package/providers/google-analytics/vite.config.ts +26 -0
- package/providers/google-analytics/vitest.config.mts +21 -0
- package/providers/medusa/package.json +3 -10
- package/providers/medusa/project.json +1 -1
- package/providers/medusa/src/providers/identity.provider.ts +34 -10
- package/providers/medusa/src/providers/profile.provider.ts +5 -15
- package/providers/medusa/src/test/test-utils.ts +0 -1
- package/providers/medusa/tsconfig.json +3 -0
- package/providers/medusa/tsconfig.lib.json +16 -1
- package/providers/meilisearch/project.json +1 -1
- package/providers/posthog/project.json +1 -1
- package/tsconfig.base.json +4 -1
- package/.claude/settings.local.json +0 -28
- package/core/src/schemas/mutations/analytics.mutation.ts +0 -23
- package/providers/algolia/src/test/test-utils.ts +0 -31
- /package/documentation/{2-getting-started.md → docs/2-getting-started.md} +0 -0
- /package/documentation/{3-querying-and-changing-data.md → docs/3-querying-and-changing-data.md} +0 -0
- /package/documentation/{4-product-data.md → docs/4-product-data.md} +0 -0
- /package/documentation/{5-cart-and-checkout.md → docs/5-cart-and-checkout.md} +0 -0
- /package/documentation/{6-product-search.md → docs/6-product-search.md} +0 -0
- /package/documentation/{7-marketing.md → docs/7-marketing.md} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Cache } from '../cache/cache.interface.js';
|
|
2
2
|
import { NoOpCache } from '../cache/noop-cache.js';
|
|
3
3
|
import type { Client } from './client.js';
|
|
4
|
-
import type
|
|
4
|
+
import { MulticastAnalyticsProvider, type AnalyticsProvider } from '../providers/analytics.provider.js';
|
|
5
5
|
import {
|
|
6
6
|
RequestContextSchema,
|
|
7
7
|
type RequestContext,
|
|
@@ -60,18 +60,14 @@ export class ClientBuilder<TClient = Client> {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
if (provider.analytics) {
|
|
63
|
-
mergedAnalytics.push(
|
|
63
|
+
mergedAnalytics.push(provider.analytics);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// Add merged analytics if any were collected
|
|
68
|
-
if (mergedAnalytics.length > 0) {
|
|
69
|
-
(client as Record<string, unknown>)['analytics'] = mergedAnalytics;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
67
|
// Add cache to complete the client
|
|
73
68
|
const completeClient = {
|
|
74
69
|
...client,
|
|
70
|
+
analytics: new MulticastAnalyticsProvider(sharedCache, this.context, mergedAnalytics),
|
|
75
71
|
cache: sharedCache,
|
|
76
72
|
} as TClient & { cache: Cache };
|
|
77
73
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { AnalyticsProvider } from "../providers/analytics.provider.js";
|
|
2
1
|
import type { ProductProvider } from "../providers/product.provider.js";
|
|
3
2
|
import type { ProductSearchProvider } from "../providers/product-search.provider.js";
|
|
4
3
|
import type { IdentityProvider } from '../providers/identity.provider.js';
|
|
@@ -7,7 +6,7 @@ import type { PriceProvider } from "../providers/price.provider.js";
|
|
|
7
6
|
import type { InventoryProvider } from "../providers/inventory.provider.js";
|
|
8
7
|
import type { Cache } from "../cache/cache.interface.js";
|
|
9
8
|
import type { CategoryProvider } from "../providers/category.provider.js";
|
|
10
|
-
import type { CheckoutProvider, OrderProvider, ProfileProvider, StoreProvider } from "../providers/index.js";
|
|
9
|
+
import type { AnalyticsProvider, CheckoutProvider, OrderProvider, ProfileProvider, StoreProvider } from "../providers/index.js";
|
|
11
10
|
import type { OrderSearchProvider } from "../providers/order-search.provider.js";
|
|
12
11
|
|
|
13
12
|
export interface Client {
|
|
@@ -17,7 +16,7 @@ export interface Client {
|
|
|
17
16
|
cache: Cache,
|
|
18
17
|
cart: CartProvider,
|
|
19
18
|
checkout: CheckoutProvider,
|
|
20
|
-
analytics:
|
|
19
|
+
analytics: AnalyticsProvider,
|
|
21
20
|
price: PriceProvider,
|
|
22
21
|
inventory: InventoryProvider,
|
|
23
22
|
category: CategoryProvider,
|
|
@@ -193,7 +193,7 @@ export function validateInput<T>(
|
|
|
193
193
|
if (!parse.success) {
|
|
194
194
|
validated = error<InvalidInputError>({
|
|
195
195
|
type: 'InvalidInput',
|
|
196
|
-
error: parse.error,
|
|
196
|
+
error: JSON.stringify(z.flattenError(parse.error)),
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
|
|
@@ -218,7 +218,7 @@ export function validateOutput(
|
|
|
218
218
|
if (!parse.success) {
|
|
219
219
|
validated = error<InvalidOutputError>({
|
|
220
220
|
type: 'InvalidOutput',
|
|
221
|
-
error: parse.error,
|
|
221
|
+
error: JSON.stringify(z.flattenError(parse.error)),
|
|
222
222
|
});
|
|
223
223
|
}
|
|
224
224
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AnonymousIdentity } from './schemas/index.js';
|
|
2
|
+
import type { RequestContext } from './schemas/session.schema.js';
|
|
2
3
|
|
|
3
4
|
export function createInitialRequestContext(): RequestContext {
|
|
4
5
|
return {
|
|
@@ -15,8 +16,15 @@ export function createInitialRequestContext(): RequestContext {
|
|
|
15
16
|
countyCode: '',
|
|
16
17
|
cityCode: '',
|
|
17
18
|
},
|
|
18
|
-
session: {
|
|
19
|
-
|
|
19
|
+
session: {
|
|
20
|
+
identityContext: {
|
|
21
|
+
identity: {
|
|
22
|
+
type: 'Anonymous'
|
|
23
|
+
} satisfies AnonymousIdentity,
|
|
24
|
+
lastUpdated: new Date(),
|
|
25
|
+
personalizationKey: crypto.randomUUID(),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
20
28
|
correlationId: '',
|
|
21
29
|
isBot: false,
|
|
22
30
|
clientIp: '',
|
|
@@ -1,7 +1,82 @@
|
|
|
1
|
+
import type { RequestContext } from '../schemas/session.schema.js';
|
|
1
2
|
import { BaseProvider } from './base.provider.js';
|
|
3
|
+
import type { Cache } from '../cache/cache.interface.js';
|
|
4
|
+
import {
|
|
5
|
+
AnalyticsMutationSchema,
|
|
6
|
+
type AnalyticsMutation,
|
|
7
|
+
type AnalyticsMutationProductAddToCartEvent,
|
|
8
|
+
type AnalyticsMutationProductDetailsViewEvent,
|
|
9
|
+
type AnalyticsMutationProductSummaryClickEvent,
|
|
10
|
+
type AnalyticsMutationProductSummaryViewEvent,
|
|
11
|
+
type AnalyticsMutationPurchaseEvent,
|
|
12
|
+
} from '../schemas/index.js';
|
|
13
|
+
import { Reactionary } from '../decorators/reactionary.decorator.js';
|
|
2
14
|
|
|
3
15
|
export abstract class AnalyticsProvider extends BaseProvider {
|
|
4
16
|
protected override getResourceName(): string {
|
|
5
17
|
return 'analytics';
|
|
6
18
|
}
|
|
19
|
+
|
|
20
|
+
public async track(event: AnalyticsMutation): Promise<void> {
|
|
21
|
+
switch (event.event) {
|
|
22
|
+
case 'product-summary-view':
|
|
23
|
+
await this.processProductSummaryView(event);
|
|
24
|
+
break;
|
|
25
|
+
case 'product-summary-click':
|
|
26
|
+
await this.processProductSummaryClick(event);
|
|
27
|
+
break;
|
|
28
|
+
case 'product-details-view':
|
|
29
|
+
await this.processProductDetailsView(event);
|
|
30
|
+
break;
|
|
31
|
+
case 'product-cart-add':
|
|
32
|
+
await this.processProductAddToCart(event);
|
|
33
|
+
break;
|
|
34
|
+
case 'purchase':
|
|
35
|
+
await this.processPurchase(event)
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected async processProductSummaryView(event: AnalyticsMutationProductSummaryViewEvent) {
|
|
41
|
+
// Default is no-op
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected async processProductSummaryClick(event: AnalyticsMutationProductSummaryClickEvent) {
|
|
45
|
+
// Default is no-op
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected async processProductDetailsView(event: AnalyticsMutationProductDetailsViewEvent) {
|
|
49
|
+
// Default is no-op
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected async processProductAddToCart(event: AnalyticsMutationProductAddToCartEvent) {
|
|
53
|
+
// Default is no-op
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected async processPurchase(event: AnalyticsMutationPurchaseEvent) {
|
|
57
|
+
// Default is no-op
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class MulticastAnalyticsProvider extends AnalyticsProvider {
|
|
62
|
+
protected providers: Array<AnalyticsProvider>;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
cache: Cache,
|
|
66
|
+
requestContext: RequestContext,
|
|
67
|
+
providers: Array<AnalyticsProvider>
|
|
68
|
+
) {
|
|
69
|
+
super(cache, requestContext);
|
|
70
|
+
|
|
71
|
+
this.providers = providers;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Reactionary({
|
|
75
|
+
inputSchema: AnalyticsMutationSchema,
|
|
76
|
+
})
|
|
77
|
+
public override async track(event: AnalyticsMutation) {
|
|
78
|
+
for (const provider of this.providers) {
|
|
79
|
+
provider.track(event);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
7
82
|
}
|
|
@@ -6,6 +6,9 @@ import type { CartQueryById } from "../schemas/queries/cart.query.js";
|
|
|
6
6
|
import type { Result } from "../schemas/result.js";
|
|
7
7
|
import { BaseProvider } from "./base.provider.js";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @group Providers
|
|
11
|
+
*/
|
|
9
12
|
export abstract class CartProvider extends BaseProvider {
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -11,6 +11,7 @@ import { BaseProvider } from "./base.provider.js";
|
|
|
11
11
|
*
|
|
12
12
|
* We only allow fetching one hierachy level at a time, for now. This is to avoid development patterns of "fetch 5000 categories in one go.."
|
|
13
13
|
*
|
|
14
|
+
* @group Foo
|
|
14
15
|
*/
|
|
15
16
|
export abstract class CategoryProvider extends BaseProvider {
|
|
16
17
|
/**
|
|
@@ -13,4 +13,9 @@ export abstract class IdentityProvider extends BaseProvider {
|
|
|
13
13
|
protected override getResourceName(): string {
|
|
14
14
|
return 'identity';
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
protected updateIdentityContext(identity: Identity) {
|
|
18
|
+
this.context.session.identityContext.lastUpdated = new Date();
|
|
19
|
+
this.context.session.identityContext.identity = identity;
|
|
20
|
+
}
|
|
16
21
|
}
|
|
@@ -3,7 +3,7 @@ import type { InferType } from '../../zod-utils.js';
|
|
|
3
3
|
|
|
4
4
|
export const InvalidInputErrorSchema = z.looseObject({
|
|
5
5
|
type: z.literal('InvalidInput'),
|
|
6
|
-
error: z.
|
|
6
|
+
error: z.string(),
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
export type InvalidInputError = InferType<typeof InvalidInputErrorSchema>;
|
|
@@ -3,7 +3,7 @@ import type { InferType } from '../../zod-utils.js';
|
|
|
3
3
|
|
|
4
4
|
export const InvalidOutputErrorSchema = z.looseObject({
|
|
5
5
|
type: z.literal('InvalidOutput'),
|
|
6
|
-
error: z.
|
|
6
|
+
error: z.string()
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
export type InvalidOutputError = InferType<typeof InvalidOutputErrorSchema>;
|
|
@@ -127,6 +127,9 @@ export const ProductSearchIdentifierSchema = z.looseObject({
|
|
|
127
127
|
categoryFilter: FacetValueIdentifierSchema.optional().describe('An optional category filter applied to the search results.'),
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Bar
|
|
132
|
+
*/
|
|
130
133
|
export const OrderSearchIdentifierSchema = z.looseObject({
|
|
131
134
|
term: z.string().describe('The search term used to find orders. Not all providers may support term-based search for orders.'),
|
|
132
135
|
partNumber: z.array(z.string()).optional().describe('An optional list part number to filter orders by specific products. Will be ANDed together.'),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { CartIdentifierSchema,
|
|
2
|
+
import { CartIdentifierSchema, IdentityIdentifierSchema, OrderIdentifierSchema, OrderInventoryStatusSchema, OrderItemIdentifierSchema, OrderStatusSchema, ProductVariantIdentifierSchema } from '../models/identifiers.model.js';
|
|
3
3
|
import { BaseModelSchema } from './base.model.js';
|
|
4
4
|
import { AddressSchema } from './profile.model.js';
|
|
5
5
|
import { ShippingMethodSchema } from './shipping-method.model.js';
|
|
@@ -34,7 +34,7 @@ export const OrderSchema = BaseModelSchema.extend({
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
export type OrderStatus =
|
|
37
|
+
export type OrderStatus = z.infer<typeof OrderStatusSchema>;
|
|
38
38
|
export type OrderInventoryStatus = InferType<typeof OrderInventoryStatusSchema>;
|
|
39
39
|
export type OrderItem = InferType<typeof OrderItemSchema>;
|
|
40
40
|
export type Order = InferType<typeof OrderSchema>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import type { InferType } from '../../../zod-utils.js';
|
|
3
|
+
import { AnalyticsMutationProductAddToCartEventSchema } from './product-add-to-cart.mutation.js';
|
|
4
|
+
import { AnalyticsMutationProductDetailsViewEventSchema } from './product-details-view.mutation.js';
|
|
5
|
+
import { AnalyticsMutationProductSummaryClickEventSchema } from './product-summary-click.mutation.js';
|
|
6
|
+
import { AnalyticsMutationProductSummaryViewEventSchema } from './product-summary-view.mutation.js';
|
|
7
|
+
import { AnalyticsMutationPurchaseEventSchema } from './purchase.mutation.js';
|
|
8
|
+
|
|
9
|
+
export const AnalyticsMutationSchema = z.discriminatedUnion('event', [
|
|
10
|
+
AnalyticsMutationProductSummaryViewEventSchema,
|
|
11
|
+
AnalyticsMutationProductSummaryClickEventSchema,
|
|
12
|
+
AnalyticsMutationProductDetailsViewEventSchema,
|
|
13
|
+
AnalyticsMutationProductAddToCartEventSchema,
|
|
14
|
+
AnalyticsMutationPurchaseEventSchema
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export type AnalyticsMutation = InferType<typeof AnalyticsMutationSchema>;
|
|
18
|
+
|
|
19
|
+
export * from './product-add-to-cart.mutation.js';
|
|
20
|
+
export * from './product-details-view.mutation.js';
|
|
21
|
+
export * from './product-summary-click.mutation.js';
|
|
22
|
+
export * from './product-summary-view.mutation.js';
|
|
23
|
+
export * from './purchase.mutation.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
ProductSearchIdentifierSchema,
|
|
4
|
+
ProductIdentifierSchema,
|
|
5
|
+
} from '../../models/identifiers.model.js';
|
|
6
|
+
import { BaseMutationSchema } from '../base.mutation.js';
|
|
7
|
+
import type { InferType } from '../../../zod-utils.js';
|
|
8
|
+
|
|
9
|
+
export const AnalyticsMutationProductAddToCartEventSchema =
|
|
10
|
+
BaseMutationSchema.extend({
|
|
11
|
+
event: z.literal('product-cart-add'),
|
|
12
|
+
source: z
|
|
13
|
+
.discriminatedUnion('type', [
|
|
14
|
+
z.object({
|
|
15
|
+
type: z.literal('search'),
|
|
16
|
+
identifier: ProductSearchIdentifierSchema,
|
|
17
|
+
}),
|
|
18
|
+
])
|
|
19
|
+
.optional(),
|
|
20
|
+
product: ProductIdentifierSchema,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type AnalyticsMutationProductAddToCartEvent = InferType<
|
|
24
|
+
typeof AnalyticsMutationProductAddToCartEventSchema
|
|
25
|
+
>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { ProductIdentifierSchema } from '../../models/identifiers.model.js';
|
|
3
|
+
import { BaseMutationSchema } from '../base.mutation.js';
|
|
4
|
+
import type { InferType } from '../../../zod-utils.js';
|
|
5
|
+
|
|
6
|
+
export const AnalyticsMutationProductDetailsViewEventSchema =
|
|
7
|
+
BaseMutationSchema.extend({
|
|
8
|
+
event: z.literal('product-details-view'),
|
|
9
|
+
product: ProductIdentifierSchema,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type AnalyticsMutationProductDetailsViewEvent = InferType<
|
|
13
|
+
typeof AnalyticsMutationProductDetailsViewEventSchema
|
|
14
|
+
>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { InferType } from '../../../zod-utils.js';
|
|
2
|
+
import {
|
|
3
|
+
ProductIdentifierSchema,
|
|
4
|
+
ProductSearchIdentifierSchema,
|
|
5
|
+
} from '../../models/identifiers.model.js';
|
|
6
|
+
import { BaseMutationSchema } from '../base.mutation.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
export const AnalyticsMutationProductSummaryClickEventSchema =
|
|
10
|
+
BaseMutationSchema.extend({
|
|
11
|
+
event: z.literal('product-summary-click'),
|
|
12
|
+
product: ProductIdentifierSchema,
|
|
13
|
+
source: z
|
|
14
|
+
.discriminatedUnion('type', [
|
|
15
|
+
z.object({
|
|
16
|
+
type: z.literal('search'),
|
|
17
|
+
identifier: ProductSearchIdentifierSchema,
|
|
18
|
+
}),
|
|
19
|
+
])
|
|
20
|
+
.optional(),
|
|
21
|
+
position: z.number().min(0),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type AnalyticsMutationProductSummaryClickEvent = InferType<
|
|
25
|
+
typeof AnalyticsMutationProductSummaryClickEventSchema
|
|
26
|
+
>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { InferType } from '../../../zod-utils.js';
|
|
2
|
+
import {
|
|
3
|
+
ProductIdentifierSchema,
|
|
4
|
+
ProductSearchIdentifierSchema,
|
|
5
|
+
} from '../../models/identifiers.model.js';
|
|
6
|
+
import { BaseMutationSchema } from '../base.mutation.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
export const AnalyticsMutationProductSummaryViewEventSchema =
|
|
10
|
+
BaseMutationSchema.extend({
|
|
11
|
+
event: z.literal('product-summary-view'),
|
|
12
|
+
source: z
|
|
13
|
+
.discriminatedUnion('type', [
|
|
14
|
+
z.object({
|
|
15
|
+
type: z.literal('search'),
|
|
16
|
+
identifier: ProductSearchIdentifierSchema,
|
|
17
|
+
}),
|
|
18
|
+
])
|
|
19
|
+
.optional(),
|
|
20
|
+
products: z.array(ProductIdentifierSchema),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type AnalyticsMutationProductSummaryViewEvent = InferType<
|
|
24
|
+
typeof AnalyticsMutationProductSummaryViewEventSchema
|
|
25
|
+
>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { BaseMutationSchema } from '../base.mutation.js';
|
|
3
|
+
import type { InferType } from '../../../zod-utils.js';
|
|
4
|
+
import { OrderSchema } from '../../models/order.model.js';
|
|
5
|
+
|
|
6
|
+
export const AnalyticsMutationPurchaseEventSchema =
|
|
7
|
+
BaseMutationSchema.extend({
|
|
8
|
+
event: z.literal('purchase'),
|
|
9
|
+
order: OrderSchema
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type AnalyticsMutationPurchaseEvent = InferType<
|
|
13
|
+
typeof AnalyticsMutationPurchaseEventSchema
|
|
14
|
+
>;
|
|
@@ -2,6 +2,9 @@ import type { InferType } from "../../zod-utils.js";
|
|
|
2
2
|
import { OrderSearchIdentifierSchema } from "../models/identifiers.model.js";
|
|
3
3
|
import { BaseQuerySchema } from "./base.query.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Foo
|
|
7
|
+
*/
|
|
5
8
|
export const OrderSearchQueryByTermSchema = BaseQuerySchema.extend({
|
|
6
9
|
search: OrderSearchIdentifierSchema
|
|
7
10
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { WebStoreIdentifierSchema } from './models/identifiers.model.js';
|
|
2
|
+
import { IdentityIdentifierSchema, WebStoreIdentifierSchema } from './models/identifiers.model.js';
|
|
3
3
|
import { CurrencySchema } from './models/currency.model.js';
|
|
4
|
+
import { IdentitySchema } from './models/identity.model.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The language and locale context for the current request.
|
|
@@ -8,9 +9,17 @@ import { CurrencySchema } from './models/currency.model.js';
|
|
|
8
9
|
export const LanguageContextSchema = z.looseObject( {
|
|
9
10
|
locale: z.string().default('en-US'),
|
|
10
11
|
currencyCode: CurrencySchema.default(() => CurrencySchema.parse({})),
|
|
11
|
-
})
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const IdentityContextSchema = z.looseObject({
|
|
15
|
+
identity: IdentitySchema,
|
|
16
|
+
personalizationKey: z.string(),
|
|
17
|
+
lastUpdated: z.date()
|
|
18
|
+
});
|
|
12
19
|
|
|
13
|
-
export const SessionSchema = z.record(z.string(), z.any())
|
|
20
|
+
export const SessionSchema = z.record(z.string(), z.any()).and(z.object({
|
|
21
|
+
identityContext: IdentityContextSchema
|
|
22
|
+
}));
|
|
14
23
|
|
|
15
24
|
export const TaxJurisdictionSchema = z.object( {
|
|
16
25
|
countryCode: z.string().default('US'),
|
|
@@ -21,7 +30,6 @@ export const TaxJurisdictionSchema = z.object( {
|
|
|
21
30
|
|
|
22
31
|
export const RequestContextSchema = z.looseObject( {
|
|
23
32
|
session: SessionSchema.default(() => SessionSchema.parse({})).describe('Read/Write session storage. Caller is responsible for persisting any changes. Providers will prefix own values'),
|
|
24
|
-
|
|
25
33
|
languageContext: LanguageContextSchema.default(() => LanguageContextSchema.parse({})).describe('ReadOnly. The language and locale context for the current request.'),
|
|
26
34
|
storeIdentifier: WebStoreIdentifierSchema.default(() => WebStoreIdentifierSchema.parse({})).describe('ReadOnly. The identifier of the current web store making the request.'),
|
|
27
35
|
taxJurisdiction: TaxJurisdictionSchema.default(() => TaxJurisdictionSchema.parse({})).describe('ReadOnly. The tax jurisdiction for the current request, typically derived from the store location or carts billing address'),
|
|
@@ -32,14 +40,18 @@ export const RequestContextSchema = z.looseObject( {
|
|
|
32
40
|
clientIp: z.string().default('').describe('The IP address of the client making the request, if available. Mostly for logging purposes'),
|
|
33
41
|
userAgent: z.string().default('').describe('The user agent string of the client making the request, if available.'),
|
|
34
42
|
referrer: z.string().default('').describe('The referrer URL, if available.'),
|
|
35
|
-
})
|
|
43
|
+
});
|
|
36
44
|
|
|
37
45
|
|
|
38
46
|
|
|
39
47
|
// Note, for this ONE type (which is effectively a dictionary), we currently don't want
|
|
40
48
|
// to strip [key: string]: unknown, hence the manual zod infer over the helper.
|
|
41
49
|
// Maybe there is a better solution, with a different typing for SessionSchema...
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
export type
|
|
50
|
+
/**
|
|
51
|
+
* @see {@link SessionSchema}
|
|
52
|
+
*/
|
|
53
|
+
export type Session = z.infer<typeof SessionSchema> & { _?: never};
|
|
54
|
+
export type LanguageContext = z.infer<typeof LanguageContextSchema> & { _?: never};
|
|
55
|
+
export type RequestContext = z.infer<typeof RequestContextSchema> & { _?: never};
|
|
56
|
+
export type TaxJurisdiction = z.infer<typeof TaxJurisdictionSchema> & { _?: never};
|
|
57
|
+
export type IdentityContext = z.infer<typeof IdentityContextSchema> & { _?: never};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Capabilities, ClientFromCapabilities } from "../schemas/capabilities.schema.js";
|
|
3
|
+
import type { Cache } from '../cache/cache.interface.js';
|
|
4
|
+
import type { RequestContext } from "../schemas/session.schema.js";
|
|
5
|
+
import { AnalyticsProvider } from "../providers/analytics.provider.js";
|
|
6
|
+
import type { AnalyticsMutation } from "../schemas/index.js";
|
|
7
|
+
import { NoOpCache } from "../cache/noop-cache.js";
|
|
8
|
+
import { createInitialRequestContext } from "../initialization.js";
|
|
9
|
+
import { ClientBuilder } from "../client/client-builder.js";
|
|
10
|
+
import type { Client } from "../client/client.js";
|
|
11
|
+
|
|
12
|
+
export class MockAnalyticsProvider extends AnalyticsProvider {
|
|
13
|
+
public events: Array<AnalyticsMutation> = [];
|
|
14
|
+
|
|
15
|
+
public override async track(event: AnalyticsMutation): Promise<void> {
|
|
16
|
+
this.events.push(event);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MockConfiguration {
|
|
21
|
+
mock?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function withMockCapabilities<T extends Partial<Capabilities>>(
|
|
25
|
+
client: Partial<Client>
|
|
26
|
+
) {
|
|
27
|
+
return (
|
|
28
|
+
cache: Cache,
|
|
29
|
+
context: RequestContext
|
|
30
|
+
): ClientFromCapabilities<T> => {
|
|
31
|
+
return client as ClientFromCapabilities<T>
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('Client Builder', () => {
|
|
36
|
+
it('should properly multicast analytics events to all analytics providers that register themselves', async () => {
|
|
37
|
+
const cache = new NoOpCache();
|
|
38
|
+
const context = createInitialRequestContext();
|
|
39
|
+
const builder = new ClientBuilder(context);
|
|
40
|
+
const analyticsProvider = new MockAnalyticsProvider(cache, context);
|
|
41
|
+
const secondaryAnalyticsProvider = new MockAnalyticsProvider(cache, context);
|
|
42
|
+
const client = builder
|
|
43
|
+
.withCache(cache)
|
|
44
|
+
.withCapability(withMockCapabilities({ analytics: analyticsProvider }))
|
|
45
|
+
.withCapability(withMockCapabilities({ analytics: secondaryAnalyticsProvider }))
|
|
46
|
+
.build();
|
|
47
|
+
|
|
48
|
+
const track = await client.analytics.track({
|
|
49
|
+
event: 'product-details-view',
|
|
50
|
+
product: {
|
|
51
|
+
key: 'P-1000'
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(analyticsProvider.events.length).toBe(1);
|
|
56
|
+
expect(analyticsProvider.events[0].event).toBe('product-details-view');
|
|
57
|
+
expect(secondaryAnalyticsProvider.events.length).toBe(1);
|
|
58
|
+
expect(secondaryAnalyticsProvider.events[0].event).toBe('product-details-view');
|
|
59
|
+
});
|
|
60
|
+
});
|
package/core/src/zod-utils.ts
CHANGED
|
@@ -16,4 +16,6 @@ export type StripIndexSignature<T> =
|
|
|
16
16
|
} :
|
|
17
17
|
T;
|
|
18
18
|
|
|
19
|
-
export type
|
|
19
|
+
export type AvoidSimplification<T> = T & { _?: never }
|
|
20
|
+
|
|
21
|
+
export type InferType<T extends z.ZodTypeAny> = AvoidSimplification<StripIndexSignature<z.infer<T>>>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Tracking customer interactions
|
|
2
|
+
Reactionary takes the approach that tracking customer data should be structured and managed, as opposed to ad-hoc in the browser. The core implication of this is that tracking should happen as a centralized, first-party stream of data to the backend, which may then in turn delegate it to the interested parties. We take this approach for a few reasons:
|
|
3
|
+
|
|
4
|
+
- Performance: the client should not have to load several megabytes of tracking scripts that may in turn bring in other tracking scripts as their first priority upon entering the site. This is treating the customer a product rather than a valued customer. The client should only send their tracking data ONCE, and the performance impact of having to collect it should be a minimal obstruction to their actual goal.
|
|
5
|
+
- Control: the system should enforce mandatory requirements such as Do Not Track in a single place, and ensure that anonymization happens as required.
|
|
6
|
+
- Structure: it should be possible to reason about what is tracked on the site, how it is tracked and when it is tracked without having to visit seven different tag managers.
|
|
7
|
+
- Security: pulling in all the embedded and inline scripts from every tag manager is a security incident waiting to happen.
|
|
8
|
+
|
|
9
|
+
To this end the client exposes a single provider in the form of `client.analytics`. This client is internally responsible for delegating events to all relevant subscribers capabilities used to build the client. This means that a single call to record a pageview or attribution will be enough, even if that data internally needs to be multiplexed to GA4, Algolia and Posthog as an example.
|