@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
title: 'My Project',
|
|
5
|
+
tagline: 'Documentation',
|
|
6
|
+
url: 'https://your-site.com',
|
|
7
|
+
baseUrl: '/',
|
|
8
|
+
onBrokenLinks: 'throw',
|
|
9
|
+
staticDirectories: [path.resolve(__dirname, 'static')],
|
|
10
|
+
i18n: {
|
|
11
|
+
defaultLocale: 'en',
|
|
12
|
+
locales: ['en'],
|
|
13
|
+
},
|
|
14
|
+
markdown: {
|
|
15
|
+
format: 'md',
|
|
16
|
+
},
|
|
17
|
+
presets: [
|
|
18
|
+
[
|
|
19
|
+
'classic',
|
|
20
|
+
{
|
|
21
|
+
docs: {
|
|
22
|
+
path: path.resolve(__dirname, 'docs'),
|
|
23
|
+
routeBasePath: '/',
|
|
24
|
+
sidebarPath: path.resolve(__dirname, 'sidebars.js'),
|
|
25
|
+
},
|
|
26
|
+
blog: false,
|
|
27
|
+
theme: {
|
|
28
|
+
customCss: path.resolve(__dirname, 'src/css/custom.css'),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
],
|
|
33
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { zod2md } from 'zod2md';
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
import * as td from 'typedoc';
|
|
4
|
+
import type { TypeDocOptions } from 'typedoc';
|
|
5
|
+
import type { PluginOptions as MarkdownPluginOptions } from 'typedoc-plugin-markdown';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
type TypedocWithMarkdownOptions = TypeDocOptions & MarkdownPluginOptions;
|
|
10
|
+
|
|
11
|
+
const projects = [
|
|
12
|
+
{
|
|
13
|
+
entry: 'core/src/schemas/index.ts',
|
|
14
|
+
title: 'Schemas',
|
|
15
|
+
output: 'documentation/docs/schemas/schemas.md',
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
for (const project of projects) {
|
|
21
|
+
const markdown = await zod2md({
|
|
22
|
+
entry: project.entry,
|
|
23
|
+
title: `${project.title} Schemas`,
|
|
24
|
+
tsconfig: './core/tsconfig.lib.json',
|
|
25
|
+
});
|
|
26
|
+
writeFileSync(project.output, markdown);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const typedocConfig = {
|
|
30
|
+
entryPoints: ['core/src/providers/*.provider.ts'],
|
|
31
|
+
tsconfig: 'core/tsconfig.lib.json',
|
|
32
|
+
plugin: ['typedoc-plugin-markdown'],
|
|
33
|
+
out: 'documentation/docs/providers',
|
|
34
|
+
outputFileStrategy: 'modules',
|
|
35
|
+
categorizeByGroup: true,
|
|
36
|
+
readme: 'none',
|
|
37
|
+
membersWithOwnFile: [],
|
|
38
|
+
modulesFileName: undefined,
|
|
39
|
+
} satisfies TypedocWithMarkdownOptions;
|
|
40
|
+
const app = await td.Application.bootstrapWithPlugins(typedocConfig);
|
|
41
|
+
|
|
42
|
+
const project = await app.convert();
|
|
43
|
+
if (project) {
|
|
44
|
+
await app.generateOutputs(project);
|
|
45
|
+
|
|
46
|
+
await fs.rm(path.join('documentation/docs/providers', 'README.md'), {
|
|
47
|
+
force: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
main();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Layout from '@theme/Layout';
|
|
2
|
+
|
|
3
|
+
export default function Home() {
|
|
4
|
+
return (
|
|
5
|
+
<Layout title="Home">
|
|
6
|
+
<main style={{ padding: '2rem' }}>
|
|
7
|
+
<h1>Welcome to My Docs</h1>
|
|
8
|
+
<p>Get started by reading the <a href="/docs/intro">introduction</a>.</p>
|
|
9
|
+
</main>
|
|
10
|
+
</Layout>
|
|
11
|
+
);
|
|
12
|
+
}
|
package/eslint.config.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/examples-node",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"types": "src/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@reactionary/core": "0.3.
|
|
8
|
-
"@reactionary/provider-commercetools": "0.3.
|
|
9
|
-
"@reactionary/provider-algolia": "0.3.
|
|
10
|
-
"@reactionary/provider-medusa": "0.3.
|
|
11
|
-
"@reactionary/provider-meilisearch": "0.3.
|
|
7
|
+
"@reactionary/core": "0.3.2",
|
|
8
|
+
"@reactionary/provider-commercetools": "0.3.2",
|
|
9
|
+
"@reactionary/provider-algolia": "0.3.2",
|
|
10
|
+
"@reactionary/provider-medusa": "0.3.2",
|
|
11
|
+
"@reactionary/provider-meilisearch": "0.3.2"
|
|
12
12
|
},
|
|
13
13
|
"type": "module"
|
|
14
14
|
}
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { ClientBuilder, createInitialRequestContext, NoOpCache } from '@reactionary/core';
|
|
3
3
|
import { FakeProductProvider, withFakeCapabilities } from '@reactionary/provider-fake';
|
|
4
4
|
import { CommercetoolsCartProvider, withCommercetoolsCapabilities } from '@reactionary/provider-commercetools';
|
|
5
|
-
import {
|
|
5
|
+
import { AlgoliaProductSearchProvider, withAlgoliaCapabilities } from '@reactionary/provider-algolia';
|
|
6
6
|
|
|
7
7
|
describe('client creation', () => {
|
|
8
8
|
it('should be able to mix providers and get a valid, typed client', async () => {
|
|
@@ -48,6 +48,6 @@ describe('client creation', () => {
|
|
|
48
48
|
|
|
49
49
|
expect(client.cart).toBeInstanceOf(CommercetoolsCartProvider);
|
|
50
50
|
expect(client.product).toBeInstanceOf(FakeProductProvider);
|
|
51
|
-
expect(client.productSearch).toBeInstanceOf(
|
|
51
|
+
expect(client.productSearch).toBeInstanceOf(AlgoliaProductSearchProvider);
|
|
52
52
|
});
|
|
53
53
|
});
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/source",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@commercetools/platform-sdk": "^8.16.0",
|
|
8
8
|
"@commercetools/ts-client": "^4.2.1",
|
|
9
|
+
"@commercetools/ts-sdk-apm": "^4.0.0",
|
|
10
|
+
"@docusaurus/core": "^3.9.2",
|
|
11
|
+
"@docusaurus/preset-classic": "^3.9.2",
|
|
9
12
|
"@faker-js/faker": "^9.8.0",
|
|
10
13
|
"@medusajs/js-sdk": "^2.13.0",
|
|
11
14
|
"@opentelemetry/api": "^1.9.0",
|
|
@@ -19,7 +22,6 @@
|
|
|
19
22
|
"meilisearch": "^0.55.0",
|
|
20
23
|
"node-object-hash": "^3.1.1",
|
|
21
24
|
"posthog-node": "^5.24.0",
|
|
22
|
-
"search-insights": "^2.17.3",
|
|
23
25
|
"zod": "4.1.9"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
@@ -44,15 +46,23 @@
|
|
|
44
46
|
"@types/debug": "^4.1.12",
|
|
45
47
|
"@types/node": "^24.0.0",
|
|
46
48
|
"@typescript-eslint/utils": "^8.33.1",
|
|
49
|
+
"@vitest/coverage-v8": "^4.0.0",
|
|
50
|
+
"docusaurus-plugin-typedoc": "^1.4.2",
|
|
51
|
+
"esbuild": "^0.19.2",
|
|
52
|
+
"jsonc-eslint-parser": "^2.1.0",
|
|
47
53
|
"nx": "22.4.5",
|
|
48
|
-
"prettier": "
|
|
54
|
+
"prettier": "~3.6.2",
|
|
49
55
|
"ts-node": "10.9.1",
|
|
50
56
|
"tslib": "^2.3.0",
|
|
57
|
+
"typedoc": "^0.28.16",
|
|
58
|
+
"typedoc-plugin-markdown": "^4.9.0",
|
|
51
59
|
"typescript": "5.9.3",
|
|
52
60
|
"typescript-eslint": "^8.33.1",
|
|
61
|
+
"verdaccio": "^6.0.5",
|
|
53
62
|
"vite": "7.1.9",
|
|
54
63
|
"vite-tsconfig-paths": "^5.1.4",
|
|
55
|
-
"vitest": "^4.0.9"
|
|
64
|
+
"vitest": "^4.0.9",
|
|
65
|
+
"zod2md": "^0.2.5"
|
|
56
66
|
},
|
|
57
67
|
"nx": {
|
|
58
68
|
"includedScripts": []
|
|
@@ -62,5 +72,9 @@
|
|
|
62
72
|
"provenance": false
|
|
63
73
|
},
|
|
64
74
|
"packageManager": "pnpm@8.15.9+sha512.499434c9d8fdd1a2794ebf4552b3b25c0a633abcee5bb15e7b5de90f32f47b513aca98cd5cfd001c31f0db454bc3804edccd578501e4ca293a6816166bbd9f81",
|
|
65
|
-
"scripts": {
|
|
75
|
+
"scripts": {
|
|
76
|
+
"docs:start": "docusaurus start --config documentation/docusaurus.config.js",
|
|
77
|
+
"docs:build": "docusaurus build --config documentation/docusaurus.config.js",
|
|
78
|
+
"docs:generate": "node ./documentation/scripts/generate.ts"
|
|
79
|
+
}
|
|
66
80
|
}
|
|
@@ -38,11 +38,19 @@ You can have more, for use with facets, and additional searchable fields, but th
|
|
|
38
38
|
|
|
39
39
|
The `objectID` corrosponds to your productIdentifier, and `variantID` should match your SKU
|
|
40
40
|
|
|
41
|
+
## Analytics
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
The Algolia analytics provider maps the following tracked event types to data tracked in Algolia:
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
- AnalyticsMutationProductSummaryViewEvent => ViewedObjectIDs
|
|
46
|
+
- AnalyticsMutationProductSummaryClickEvent => ClickedObjectIDsAfterSearch / ClickedObjectIDs
|
|
47
|
+
- AnalyticsMutationProductAddToCartEvent => AddedToCartObjectIDsAfterSearch / AddedToCartObjectIDs
|
|
48
|
+
- AnalyticsMutationPurchaseEvent => PurchasedObjectIDs
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
The `AfterSearch` variants are (with the exception of purchase) preferred by the provider in the cases where Algolia is the source of the events. For search or recommendation this would typically be the case, but not necesarily for users arriving on a PDP as a direct target from a search or a link.
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
Note that we do not map `PurchasedObjectIDsAfterSearch` as it would require us to persist the search query ID that lead to the add-to-cart occuring on the cart items. This currently seems like an excess burden to impose on the cart interface.
|
|
53
|
+
|
|
54
|
+
The `ConvertedObjectIDs` and `ConvertedObjectIDsAfterSearch` are not mapped as they seem superfluous by all accounts in a product-purchase based flow. They could likely be used for other types of conversions in a more general setup, such as a customer finishing reading an article.
|
|
55
|
+
|
|
56
|
+
Finally the events that are related to filtering are not mapped, as they are by all accounts deprecated and no longer influence any of the recommendation or personalization features.
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import type { Cache, ClientFromCapabilities, RequestContext } from "@reactionary/core";
|
|
2
|
-
import {
|
|
2
|
+
import { AlgoliaProductSearchProvider } from "../providers/product-search.provider.js";
|
|
3
3
|
import type { AlgoliaCapabilities } from "../schema/capabilities.schema.js";
|
|
4
4
|
import type { AlgoliaConfiguration } from "../schema/configuration.schema.js";
|
|
5
|
+
import { AlgoliaAnalyticsProvider } from "../providers/analytics.provider.js";
|
|
5
6
|
|
|
6
7
|
export function withAlgoliaCapabilities<T extends AlgoliaCapabilities>(configuration: AlgoliaConfiguration, capabilities: T) {
|
|
7
8
|
return (cache: Cache, context: RequestContext): ClientFromCapabilities<T> => {
|
|
8
9
|
const client: any = {};
|
|
9
10
|
|
|
10
11
|
if (capabilities.productSearch) {
|
|
11
|
-
client.productSearch = new
|
|
12
|
+
client.productSearch = new AlgoliaProductSearchProvider(cache, context, configuration);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (capabilities.analytics) {
|
|
16
|
+
client.analytics = new AlgoliaAnalyticsProvider(cache, context, configuration);
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
return client;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnalyticsProvider,
|
|
3
|
+
type AnalyticsMutationProductAddToCartEvent,
|
|
4
|
+
type AnalyticsMutationProductSummaryClickEvent,
|
|
5
|
+
type AnalyticsMutationProductSummaryViewEvent,
|
|
6
|
+
type AnalyticsMutationPurchaseEvent,
|
|
7
|
+
type Cache,
|
|
8
|
+
type RequestContext,
|
|
9
|
+
} from '@reactionary/core';
|
|
10
|
+
import {
|
|
11
|
+
type InsightsClient,
|
|
12
|
+
type ViewedObjectIDs,
|
|
13
|
+
type ClickedObjectIDsAfterSearch,
|
|
14
|
+
type AddedToCartObjectIDsAfterSearch,
|
|
15
|
+
type PurchasedObjectIDs,
|
|
16
|
+
algoliasearch,
|
|
17
|
+
} from 'algoliasearch';
|
|
18
|
+
import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
|
|
19
|
+
import type { AlgoliaProductSearchIdentifier } from '../schema/search.schema.js';
|
|
20
|
+
|
|
21
|
+
export class AlgoliaAnalyticsProvider extends AnalyticsProvider {
|
|
22
|
+
protected client: InsightsClient;
|
|
23
|
+
protected config: AlgoliaConfiguration;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
cache: Cache,
|
|
27
|
+
requestContext: RequestContext,
|
|
28
|
+
config: AlgoliaConfiguration
|
|
29
|
+
) {
|
|
30
|
+
super(cache, requestContext);
|
|
31
|
+
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.client = algoliasearch(this.config.appId, this.config.apiKey).initInsights({});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
protected override async processProductAddToCart(
|
|
37
|
+
event: AnalyticsMutationProductAddToCartEvent
|
|
38
|
+
) {
|
|
39
|
+
if (event.source && event.source.type === 'search') {
|
|
40
|
+
const algoliaEvent = {
|
|
41
|
+
eventName: 'addToCart',
|
|
42
|
+
eventType: 'conversion',
|
|
43
|
+
eventSubtype: 'addToCart',
|
|
44
|
+
index: this.config.indexName,
|
|
45
|
+
objectIDs: [event.product.key],
|
|
46
|
+
userToken: this.context.session.identityContext.personalizationKey,
|
|
47
|
+
queryID: (event.source.identifier as AlgoliaProductSearchIdentifier)
|
|
48
|
+
.key,
|
|
49
|
+
} satisfies AddedToCartObjectIDsAfterSearch;
|
|
50
|
+
|
|
51
|
+
const response = await this.client.pushEvents({
|
|
52
|
+
events: [algoliaEvent],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected override async processProductSummaryClick(
|
|
58
|
+
event: AnalyticsMutationProductSummaryClickEvent
|
|
59
|
+
) {
|
|
60
|
+
if (event.source && event.source.type === 'search') {
|
|
61
|
+
const algoliaEvent = {
|
|
62
|
+
eventName: 'click',
|
|
63
|
+
eventType: 'click',
|
|
64
|
+
index: this.config.indexName,
|
|
65
|
+
objectIDs: [event.product.key],
|
|
66
|
+
userToken: this.context.session.identityContext.personalizationKey,
|
|
67
|
+
positions: [event.position],
|
|
68
|
+
queryID: (event.source.identifier as AlgoliaProductSearchIdentifier)
|
|
69
|
+
.key,
|
|
70
|
+
} satisfies ClickedObjectIDsAfterSearch;
|
|
71
|
+
|
|
72
|
+
const response = await this.client.pushEvents({
|
|
73
|
+
events: [algoliaEvent],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected override async processProductSummaryView(
|
|
79
|
+
event: AnalyticsMutationProductSummaryViewEvent
|
|
80
|
+
) {
|
|
81
|
+
if (event.source && event.source.type === 'search') {
|
|
82
|
+
const algoliaEvent = {
|
|
83
|
+
eventName: 'view',
|
|
84
|
+
eventType: 'view',
|
|
85
|
+
index: this.config.indexName,
|
|
86
|
+
objectIDs: event.products.map((x) => x.key),
|
|
87
|
+
userToken: this.context.session.identityContext.personalizationKey,
|
|
88
|
+
} satisfies ViewedObjectIDs;
|
|
89
|
+
|
|
90
|
+
const response = await this.client.pushEvents({
|
|
91
|
+
events: [algoliaEvent],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
protected override async processPurchase(
|
|
97
|
+
event: AnalyticsMutationPurchaseEvent
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
// TODO: Figure out how to handle the problem below. From the order we have the SKUs,
|
|
100
|
+
// but in Algolia we have the products indexed, and we can't really resolve it here...
|
|
101
|
+
const algoliaEvent = {
|
|
102
|
+
eventName: 'purchase',
|
|
103
|
+
eventType: 'conversion',
|
|
104
|
+
eventSubtype: 'purchase',
|
|
105
|
+
index: this.config.indexName,
|
|
106
|
+
objectIDs: event.order.items.map((x) => x.variant.sku),
|
|
107
|
+
userToken: this.context.session.identityContext.personalizationKey,
|
|
108
|
+
} satisfies PurchasedObjectIDs;
|
|
109
|
+
|
|
110
|
+
const response = await this.client.pushEvents({
|
|
111
|
+
events: [algoliaEvent],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -40,10 +40,10 @@ interface AlgoliaNativeRecord {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
export class
|
|
43
|
+
export class AlgoliaProductSearchProvider extends ProductSearchProvider {
|
|
44
44
|
protected config: AlgoliaConfiguration;
|
|
45
45
|
|
|
46
|
-
constructor(
|
|
46
|
+
constructor(cache: Cache, context: RequestContext, config: AlgoliaConfiguration) {
|
|
47
47
|
super(cache, context);
|
|
48
48
|
this.config = config;
|
|
49
49
|
}
|
|
@@ -195,7 +195,8 @@ export class AlgoliaSearchProvider extends ProductSearchProvider {
|
|
|
195
195
|
facets: query.search.facets,
|
|
196
196
|
filters: query.search.filters,
|
|
197
197
|
paginationOptions: query.search.paginationOptions,
|
|
198
|
-
|
|
198
|
+
index: body.index || '',
|
|
199
|
+
key: body.queryID || '',
|
|
199
200
|
},
|
|
200
201
|
pageNumber: (body.page || 0) + 1,
|
|
201
202
|
pageSize: body.hitsPerPage || 0,
|
|
@@ -203,7 +204,7 @@ export class AlgoliaSearchProvider extends ProductSearchProvider {
|
|
|
203
204
|
totalPages: body.nbPages || 0,
|
|
204
205
|
items: items,
|
|
205
206
|
facets,
|
|
206
|
-
} satisfies
|
|
207
|
+
} satisfies AlgoliaProductSearchResult;
|
|
207
208
|
|
|
208
209
|
return result;
|
|
209
210
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, assert } from 'vitest';
|
|
2
|
+
import { AlgoliaAnalyticsProvider } from '../providers/analytics.provider.js';
|
|
3
|
+
import { createInitialRequestContext, NoOpCache } from '@reactionary/core';
|
|
4
|
+
import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
|
|
5
|
+
import { AlgoliaProductSearchProvider } from '../providers/product-search.provider.js';
|
|
6
|
+
|
|
7
|
+
describe('Analytics event tracking', async () => {
|
|
8
|
+
const config = {
|
|
9
|
+
apiKey: process.env['ALGOLIA_API_KEY'] || '',
|
|
10
|
+
appId: process.env['ALGOLIA_APP_ID'] || '',
|
|
11
|
+
indexName: process.env['ALGOLIA_INDEX'] || '',
|
|
12
|
+
} satisfies AlgoliaConfiguration;
|
|
13
|
+
const cache = new NoOpCache();
|
|
14
|
+
const context = createInitialRequestContext();
|
|
15
|
+
|
|
16
|
+
const search = new AlgoliaProductSearchProvider(cache, context, config);
|
|
17
|
+
const analytics = new AlgoliaAnalyticsProvider(cache, context, config);
|
|
18
|
+
const searchResult = await search.queryByTerm({
|
|
19
|
+
search: {
|
|
20
|
+
facets: [],
|
|
21
|
+
filters: [],
|
|
22
|
+
paginationOptions: {
|
|
23
|
+
pageNumber: 1,
|
|
24
|
+
pageSize: 10,
|
|
25
|
+
},
|
|
26
|
+
term: 'q',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!searchResult.success) {
|
|
31
|
+
assert.fail();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it('can track summary clicks', async () => {
|
|
35
|
+
await analytics.track({
|
|
36
|
+
event: 'product-summary-click',
|
|
37
|
+
product: searchResult.value.items[0].identifier,
|
|
38
|
+
position: 1,
|
|
39
|
+
source: {
|
|
40
|
+
type: 'search',
|
|
41
|
+
identifier: searchResult.value.identifier,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('can track summary views', async () => {
|
|
47
|
+
await analytics.track({
|
|
48
|
+
event: 'product-summary-view',
|
|
49
|
+
products: searchResult.value.items.map((x) => x.identifier),
|
|
50
|
+
source: {
|
|
51
|
+
type: 'search',
|
|
52
|
+
identifier: searchResult.value.identifier,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('can track add to cart', async () => {
|
|
58
|
+
await analytics.track({
|
|
59
|
+
event: 'product-cart-add',
|
|
60
|
+
product: searchResult.value.items[0].identifier,
|
|
61
|
+
source: {
|
|
62
|
+
type: 'search',
|
|
63
|
+
identifier: searchResult.value.identifier,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('can track purchase', async () => {
|
|
69
|
+
await analytics.track({
|
|
70
|
+
event: 'purchase',
|
|
71
|
+
order: {
|
|
72
|
+
identifier: {
|
|
73
|
+
key: crypto.randomUUID(),
|
|
74
|
+
},
|
|
75
|
+
inventoryStatus: 'Allocated',
|
|
76
|
+
items: [
|
|
77
|
+
{
|
|
78
|
+
identifier: {
|
|
79
|
+
key: crypto.randomUUID(),
|
|
80
|
+
},
|
|
81
|
+
inventoryStatus: 'Allocated',
|
|
82
|
+
price: {
|
|
83
|
+
unitPrice: {
|
|
84
|
+
currency: 'USD',
|
|
85
|
+
value: 50,
|
|
86
|
+
},
|
|
87
|
+
totalDiscount: {
|
|
88
|
+
currency: 'USD',
|
|
89
|
+
value: 0,
|
|
90
|
+
},
|
|
91
|
+
totalPrice: {
|
|
92
|
+
currency: 'USD',
|
|
93
|
+
value: 50,
|
|
94
|
+
},
|
|
95
|
+
unitDiscount: {
|
|
96
|
+
currency: 'USD',
|
|
97
|
+
value: 0,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
quantity: 1,
|
|
101
|
+
variant: searchResult.value.items[0].variants[0].variant,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
orderStatus: 'Shipped',
|
|
105
|
+
paymentInstructions: [],
|
|
106
|
+
price: {
|
|
107
|
+
grandTotal: {
|
|
108
|
+
currency: 'USD',
|
|
109
|
+
value: 50,
|
|
110
|
+
},
|
|
111
|
+
totalDiscount: {
|
|
112
|
+
currency: 'USD',
|
|
113
|
+
value: 0,
|
|
114
|
+
},
|
|
115
|
+
totalProductPrice: {
|
|
116
|
+
currency: 'USD',
|
|
117
|
+
value: 50,
|
|
118
|
+
},
|
|
119
|
+
totalShipping: {
|
|
120
|
+
currency: 'USD',
|
|
121
|
+
value: 0,
|
|
122
|
+
},
|
|
123
|
+
totalSurcharge: {
|
|
124
|
+
currency: 'USD',
|
|
125
|
+
value: 0,
|
|
126
|
+
},
|
|
127
|
+
totalTax: {
|
|
128
|
+
currency: 'USD',
|
|
129
|
+
value: 0,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
userId: {
|
|
133
|
+
userId: crypto.randomUUID()
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
success,
|
|
16
16
|
} from '@reactionary/core';
|
|
17
17
|
import type { CommercetoolsConfiguration } from '../schema/configuration.schema.js';
|
|
18
|
-
import type z from 'zod';
|
|
19
18
|
import type { CommercetoolsAPI } from '../core/client.js';
|
|
20
19
|
|
|
21
20
|
export class CommercetoolsIdentityProvider extends IdentityProvider {
|
|
@@ -41,6 +40,8 @@ export class CommercetoolsIdentityProvider extends IdentityProvider {
|
|
|
41
40
|
public override async getSelf(payload: IdentityQuerySelf): Promise<Result<Identity>> {
|
|
42
41
|
const identity = await this.commercetools.introspect();
|
|
43
42
|
|
|
43
|
+
this.updateIdentityContext(identity);
|
|
44
|
+
|
|
44
45
|
return success(identity);
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -54,6 +55,8 @@ export class CommercetoolsIdentityProvider extends IdentityProvider {
|
|
|
54
55
|
payload.password
|
|
55
56
|
);
|
|
56
57
|
|
|
58
|
+
this.updateIdentityContext(identity);
|
|
59
|
+
|
|
57
60
|
return success(identity);
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -63,6 +66,8 @@ export class CommercetoolsIdentityProvider extends IdentityProvider {
|
|
|
63
66
|
public override async logout(payload: Record<string, never>): Promise<Result<Identity>> {
|
|
64
67
|
const identity = await this.commercetools.logout();
|
|
65
68
|
|
|
69
|
+
this.updateIdentityContext(identity);
|
|
70
|
+
|
|
66
71
|
return success(identity);
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -78,6 +83,8 @@ export class CommercetoolsIdentityProvider extends IdentityProvider {
|
|
|
78
83
|
payload.password
|
|
79
84
|
);
|
|
80
85
|
|
|
86
|
+
this.updateIdentityContext(identity);
|
|
87
|
+
|
|
81
88
|
return success(identity);
|
|
82
89
|
}
|
|
83
90
|
}
|
|
@@ -237,10 +237,7 @@ export class CommercetoolsProfileProvider extends ProfileProvider {
|
|
|
237
237
|
if (addressToMakeDefault.id === customer.defaultBillingAddressId) {
|
|
238
238
|
return error<InvalidInputError>({
|
|
239
239
|
type: 'InvalidInput',
|
|
240
|
-
error:
|
|
241
|
-
field: 'addressIdentifier',
|
|
242
|
-
message: 'Cannot set shipping address as default billing address',
|
|
243
|
-
}
|
|
240
|
+
error: 'Cannot set shipping address as default billing address'
|
|
244
241
|
});
|
|
245
242
|
}
|
|
246
243
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type ProductSearchQueryByTerm,
|
|
9
9
|
} from '@reactionary/core';
|
|
10
10
|
import { CommercetoolsProductProvider } from '../providers/product.provider.js';
|
|
11
|
-
import {
|
|
11
|
+
import { CommercetoolsAPI } from '../core/client.js';
|
|
12
12
|
import { CommercetoolsSearchProvider } from '../providers/product-search.provider.js';
|
|
13
13
|
|
|
14
14
|
describe('Caching', () => {
|
|
@@ -16,7 +16,7 @@ describe('Caching', () => {
|
|
|
16
16
|
const config = getCommercetoolsTestConfiguration();
|
|
17
17
|
const context = createInitialRequestContext();
|
|
18
18
|
const cache = new MemoryCache();
|
|
19
|
-
const client = new
|
|
19
|
+
const client = new CommercetoolsAPI(config, context);
|
|
20
20
|
const provider = new CommercetoolsProductProvider(config, cache, context, client);
|
|
21
21
|
|
|
22
22
|
const identifier = {
|
|
@@ -48,7 +48,7 @@ describe('Caching', () => {
|
|
|
48
48
|
const config = getCommercetoolsTestConfiguration();
|
|
49
49
|
const context = createInitialRequestContext();
|
|
50
50
|
const cache = new MemoryCache();
|
|
51
|
-
const client = new
|
|
51
|
+
const client = new CommercetoolsAPI(config, context);
|
|
52
52
|
const provider = new CommercetoolsSearchProvider(config, cache, context, client);
|
|
53
53
|
|
|
54
54
|
const query = {
|