@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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
AnalyticsProvider,
|
|
3
|
+
productKey,
|
|
4
|
+
searchOriginOf
|
|
15
5
|
} from "@reactionary/core";
|
|
16
|
-
import {
|
|
6
|
+
import { hash } from "node:crypto";
|
|
17
7
|
import { UnomiAPI } from "../core/client.js";
|
|
18
|
-
class UnomiAnalyticsCapability extends
|
|
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
|
-
|
|
27
|
-
return
|
|
27
|
+
getResourceName() {
|
|
28
|
+
return "unomi-analytics";
|
|
28
29
|
}
|
|
29
|
-
async
|
|
30
|
-
const events =
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
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.
|
|
12
|
+
"@reactionary/core": "0.12.0",
|
|
13
13
|
"zod": "4.1.9"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
protected
|
|
11
|
-
protected
|
|
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 {
|
|
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:
|
|
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 {
|
|
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) =>
|
|
30
|
+
capability?: (args: UnomiCapabilityFactoryArgs) => AnalyticsProvider;
|
|
31
31
|
}
|