@reactionary/unomi 0.11.1 → 0.12.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.
@@ -1,154 +1,143 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __decorateClass = (decorators, target, key, kind) => {
4
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
- if (decorator = decorators[i])
7
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
- if (kind && result) __defProp(target, key, result);
9
- return result;
10
- };
11
1
  import {
12
- AnalyticsCapability,
13
- AnalyticsMutationSchema,
14
- AnalyticsResultSchema
2
+ AnalyticsProvider,
3
+ productKey,
4
+ searchOriginOf
15
5
  } from "@reactionary/core";
16
- import { Reactionary } from "@reactionary/core";
6
+ import { hash } from "node:crypto";
17
7
  import { UnomiAPI } from "../core/client.js";
18
- class UnomiAnalyticsCapability extends AnalyticsCapability {
8
+ class UnomiAnalyticsCapability extends AnalyticsProvider {
19
9
  constructor(cache, context, config) {
20
10
  super(cache, context);
21
11
  this.config = config;
22
12
  this.api = new UnomiAPI(config, context);
23
13
  }
24
14
  config;
15
+ types = [
16
+ "search-results-viewed",
17
+ "product-carousel-viewed",
18
+ "product-viewed",
19
+ "product-clicked",
20
+ "cart-item-added",
21
+ "order-placed",
22
+ "user-logged-in"
23
+ ];
24
+ /** A CDP builds visitor profiles for personalization. */
25
+ purpose = "marketing";
25
26
  api;
26
- async track(event) {
27
- return super.track(event);
27
+ getResourceName() {
28
+ return "unomi-analytics";
28
29
  }
29
- async processProductSummaryView(event) {
30
- const events = event.products.map((product) => ({
31
- eventType: "product_view",
32
- scope: this.config.scope,
33
- target: {
34
- itemId: product.key,
35
- itemType: "product"
36
- }
37
- }));
38
- try {
39
- await this.postEvents(events);
40
- return this.accepted();
41
- } catch {
42
- return this.rejected();
30
+ async handle(event, ctx) {
31
+ const events = await this.toUnomiEvents(event, ctx);
32
+ if (events.length === 0) {
33
+ return "ignored";
43
34
  }
44
- }
45
- async processProductSummaryClick(event) {
46
- const events = [
47
- {
48
- eventType: "selectItem",
49
- scope: this.config.scope,
50
- targetProperties: {
51
- productId: event.product.key,
52
- sku: event.product.key,
53
- index: event.position
54
- }
55
- }
56
- ];
57
- try {
58
- await this.postEvents(events);
59
- return this.accepted();
60
- } catch {
61
- return this.rejected();
35
+ const profileId = event.type === "user-logged-in" ? event.payload.previousMarketingId?.key || event.context.marketingIdentifier.key : event.context.marketingIdentifier.key;
36
+ if (!profileId) {
37
+ return "ignored";
62
38
  }
39
+ await this.postEvents(profileId, events);
40
+ return "accepted";
63
41
  }
64
- async processProductDetailsView(event) {
65
- const events = [
66
- {
67
- eventType: "product_view",
68
- scope: this.config.scope,
69
- target: {
70
- itemId: event.product.key,
71
- itemType: "product"
42
+ async toUnomiEvents(event, ctx) {
43
+ const scope = this.config.scope;
44
+ switch (event.type) {
45
+ case "search-results-viewed":
46
+ case "product-carousel-viewed":
47
+ return event.payload.products.map((product) => ({
48
+ eventType: "product_view",
49
+ scope,
50
+ target: { itemId: productKey(product), itemType: "product" }
51
+ }));
52
+ case "product-viewed":
53
+ return [{ eventType: "product_view", scope, target: { itemId: productKey(event.payload.product), itemType: "product" } }];
54
+ case "product-clicked": {
55
+ const search = searchOriginOf(event.origin);
56
+ return [
57
+ {
58
+ eventType: "selectItem",
59
+ scope,
60
+ target: { itemId: productKey(event.payload.product), itemType: "product" },
61
+ properties: search ? { index: search.position, searchTerm: search.identifier.term } : void 0
62
+ }
63
+ ];
64
+ }
65
+ case "cart-item-added": {
66
+ const line = event.payload.cart.items.find((item) => item.variant.sku === event.payload.variant.sku);
67
+ if (!line) {
68
+ return [];
72
69
  }
70
+ const product = await ctx.products.byId(line.product);
71
+ return [
72
+ {
73
+ eventType: "addToCart",
74
+ scope,
75
+ target: {
76
+ itemId: line.product.key,
77
+ itemType: "product",
78
+ customItemType: "product",
79
+ properties: {
80
+ sku: line.variant.sku,
81
+ price: line.price.unitPrice.value,
82
+ currency: line.price.unitPrice.currency,
83
+ quantity: event.payload.quantity,
84
+ ...product.success ? { productName: product.value.name } : {}
85
+ }
86
+ }
87
+ }
88
+ ];
73
89
  }
74
- ];
75
- try {
76
- await this.postEvents(events);
77
- return this.accepted();
78
- } catch {
79
- return this.rejected();
80
- }
81
- }
82
- async processProductAddToCart(event) {
83
- const events = [
84
- {
85
- eventType: "addToCart",
86
- scope: this.config.scope,
87
- target: {
88
- itemId: event.product.key,
89
- itemType: "product",
90
- customItemType: "product",
91
- properties: {
92
- price: 1,
93
- currency: this.context.languageContext.currencyCode ?? "USD",
94
- productName: "-"
90
+ case "order-placed": {
91
+ const order = event.payload.order;
92
+ return [
93
+ {
94
+ eventType: "purchaseComplete",
95
+ scope,
96
+ target: { itemId: order.identifier.key, itemType: "order" },
97
+ properties: {
98
+ orderId: order.identifier.key,
99
+ totalAmount: order.price.grandTotal.value,
100
+ currency: order.price.grandTotal.currency,
101
+ itemCount: order.items.length,
102
+ items: order.items.map((item) => ({
103
+ sku: item.variant.sku,
104
+ quantity: item.quantity,
105
+ price: item.price.unitPrice.value
106
+ }))
107
+ }
95
108
  }
96
- }
109
+ ];
97
110
  }
98
- ];
99
- try {
100
- await this.postEvents(events);
101
- return this.accepted();
102
- } catch (e) {
103
- console.error("Error posting addToCart event to Unomi:", e);
104
- return this.rejected();
105
- }
106
- }
107
- async processPurchase(event) {
108
- const events = [
109
- {
110
- eventType: "purchaseComplete",
111
- scope: this.config.scope,
112
- targetProperties: {
113
- totalAmount: event.order.price.grandTotal.value,
114
- currency: event.order.price.grandTotal.currency,
115
- itemCount: event.order.items.length
116
- }
111
+ case "user-logged-in": {
112
+ const identity = event.payload.identity;
113
+ return [
114
+ {
115
+ eventType: "login",
116
+ scope,
117
+ // An opaque hash: the CDP should not receive the backend user id itself.
118
+ properties: identity.type === "Anonymous" ? void 0 : { userId: hash("sha256", identity.id.userId) }
119
+ }
120
+ ];
117
121
  }
118
- ];
119
- try {
120
- await this.postEvents(events);
121
- return this.accepted();
122
- } catch {
123
- return this.rejected();
122
+ default:
123
+ return [];
124
124
  }
125
125
  }
126
- async postEvents(events) {
127
- const profileId = this.context.session.marketingContext.identifier.key;
128
- const source = {
129
- itemType: "page",
130
- scope: this.config.scope,
131
- itemId: "backend"
132
- };
133
- const payload = {
134
- source,
126
+ async postEvents(profileId, events) {
127
+ const response = await this.api.postEvent({
128
+ source: {
129
+ itemType: "page",
130
+ scope: this.config.scope,
131
+ itemId: "backend"
132
+ },
135
133
  profileId,
136
134
  events
137
- };
138
- const s = JSON.stringify(payload, null, 2);
139
- console.dir(payload, { depth: null });
140
- const response = await this.api.postEvent(payload);
135
+ });
141
136
  if (!response.ok) {
142
137
  throw new Error(`Unomi event request failed with status ${response.status}`);
143
138
  }
144
139
  }
145
140
  }
146
- __decorateClass([
147
- Reactionary({
148
- inputSchema: AnalyticsMutationSchema,
149
- outputSchema: AnalyticsResultSchema
150
- })
151
- ], UnomiAnalyticsCapability.prototype, "track", 1);
152
141
  export {
153
142
  UnomiAnalyticsCapability
154
143
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/unomi",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./src/index.d.ts",
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/Solteq/reactionary.git"
10
10
  },
11
11
  "dependencies": {
12
- "@reactionary/core": "0.11.1",
12
+ "@reactionary/core": "0.12.0",
13
13
  "zod": "4.1.9"
14
14
  },
15
15
  "devDependencies": {
@@ -1,15 +1,28 @@
1
- import { AnalyticsCapability, type AnalyticsMutation, type AnalyticsMutationProductAddToCartEvent, type AnalyticsMutationProductDetailsViewEvent, type AnalyticsMutationProductSummaryClickEvent, type AnalyticsMutationProductSummaryViewEvent, type AnalyticsMutationPurchaseEvent, type AnalyticsResult, type Cache, type RequestContext } from '@reactionary/core';
1
+ import { AnalyticsProvider, type AnalyticsContext, type AnalyticsEvent, type AnalyticsEventType, type AnalyticsOutcome, type AnalyticsPurpose, type Cache, type RequestContext } from '@reactionary/core';
2
2
  import { UnomiAPI } from '../core/client.js';
3
3
  import type { UnomiConfiguration } from '../schema/configuration.schema.js';
4
- export declare class UnomiAnalyticsCapability extends AnalyticsCapability {
4
+ interface UnomiEvent {
5
+ eventType: string;
6
+ scope: string;
7
+ target?: Record<string, unknown>;
8
+ properties?: Record<string, unknown>;
9
+ }
10
+ /**
11
+ * Sends behavioural events to Apache Unomi's context endpoint, attributed to the visitor's marketing profile.
12
+ *
13
+ * Events are attributed to the marketing identifier captured when the event was emitted. Without one there is no profile to
14
+ * attribute to (Unomi would create a throwaway profile per request), so the event is ignored.
15
+ */
16
+ export declare class UnomiAnalyticsCapability extends AnalyticsProvider {
5
17
  protected readonly config: UnomiConfiguration;
18
+ readonly types: ReadonlyArray<AnalyticsEventType | `${string}*`>;
19
+ /** A CDP builds visitor profiles for personalization. */
20
+ readonly purpose: AnalyticsPurpose;
6
21
  protected readonly api: UnomiAPI;
7
22
  constructor(cache: Cache, context: RequestContext, config: UnomiConfiguration);
8
- track(event: AnalyticsMutation): Promise<AnalyticsResult>;
9
- protected processProductSummaryView(event: AnalyticsMutationProductSummaryViewEvent): Promise<AnalyticsResult>;
10
- protected processProductSummaryClick(event: AnalyticsMutationProductSummaryClickEvent): Promise<AnalyticsResult>;
11
- protected processProductDetailsView(event: AnalyticsMutationProductDetailsViewEvent): Promise<AnalyticsResult>;
12
- protected processProductAddToCart(event: AnalyticsMutationProductAddToCartEvent): Promise<AnalyticsResult>;
13
- protected processPurchase(event: AnalyticsMutationPurchaseEvent): Promise<AnalyticsResult>;
14
- protected postEvents(events: Array<unknown>): Promise<void>;
23
+ protected getResourceName(): string;
24
+ handle(event: AnalyticsEvent, ctx: AnalyticsContext): Promise<AnalyticsOutcome | void>;
25
+ protected toUnomiEvents(event: AnalyticsEvent, ctx: AnalyticsContext): Promise<UnomiEvent[]>;
26
+ protected postEvents(profileId: string, events: UnomiEvent[]): Promise<void>;
15
27
  }
28
+ export {};
@@ -1,4 +1,4 @@
1
- import type { AnalyticsCapability, ClientFromCapabilities, PersonalizationProfileFactory } from '@reactionary/core';
1
+ import type { AnalyticsProvider, ClientFromCapabilities, PersonalizationProfileFactory } from '@reactionary/core';
2
2
  import type { UnomiPersonalizationProfileCapability } from '../capabilities/personalization-profile.capability.js';
3
3
  import type { UnomiPersonalizationProfileFactory } from '../factories/personalization-profile/personalization-profile.factory.js';
4
4
  import type { UnomiCapabilities } from '../schema/capabilities.schema.js';
@@ -21,7 +21,7 @@ type ResolvedFactoryMap<T extends UnomiCapabilities> = {
21
21
  personalizationProfile: ExtractCapabilityFactory<T['personalizationProfile']>;
22
22
  };
23
23
  type DefaultCapabilityMap<T extends UnomiCapabilities> = {
24
- analytics: AnalyticsCapability;
24
+ analytics: AnalyticsProvider;
25
25
  personalizationProfile: UnomiPersonalizationProfileCapability<ResolvedFactoryMap<T>['personalizationProfile']>;
26
26
  };
27
27
  type CapabilityImplementationMap<T extends UnomiCapabilities> = {
@@ -1,4 +1,4 @@
1
- import type { AnalyticsCapability, PersonalizationProfileCapability } from '@reactionary/core';
1
+ import type { AnalyticsProvider, PersonalizationProfileCapability } from '@reactionary/core';
2
2
  import * as z from 'zod';
3
3
  import type { UnomiConfiguration } from './configuration.schema.js';
4
4
  import type { UnomiPersonalizationProfileFactory } from '../factories/personalization-profile/personalization-profile.factory.js';
@@ -27,5 +27,5 @@ export interface UnomiPersonalizationProfileCapabilityConfig {
27
27
  }
28
28
  export interface UnomiAnalyticsCapabilityConfig {
29
29
  enabled: boolean;
30
- capability?: (args: UnomiCapabilityFactoryArgs) => AnalyticsCapability;
30
+ capability?: (args: UnomiCapabilityFactoryArgs) => AnalyticsProvider;
31
31
  }