@reactionary/core 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.
- package/analytics/context.js +173 -0
- package/analytics/dispatcher.js +181 -0
- package/analytics/index.js +6 -0
- package/analytics/legacy-adapter.js +48 -0
- package/analytics/origins.js +64 -0
- package/analytics/recording-subscriber.js +26 -0
- package/analytics/refs.js +14 -0
- package/capabilities/analytics.capability.js +265 -84
- package/capabilities/analytics.provider.js +13 -0
- package/capabilities/index.js +1 -0
- package/client/client-builder.js +45 -16
- package/index.js +1 -0
- package/package.json +1 -1
- package/schemas/models/analytics/consent.model.js +10 -0
- package/schemas/models/analytics/dispatch.model.js +15 -0
- package/schemas/models/analytics/envelope.model.js +38 -0
- package/schemas/models/analytics/event-parts.model.js +47 -0
- package/schemas/models/analytics/event.model.js +255 -0
- package/schemas/models/analytics/index.js +6 -0
- package/schemas/models/analytics/origin.model.js +113 -0
- package/schemas/models/analytics.model.js +0 -2
- package/schemas/models/index.js +1 -0
- package/schemas/mutations/analytics/events/account.mutation.js +66 -0
- package/schemas/mutations/analytics/events/base.mutation.js +10 -0
- package/schemas/mutations/analytics/events/browse.mutation.js +53 -0
- package/schemas/mutations/analytics/events/commerce.mutation.js +79 -0
- package/schemas/mutations/analytics/events/index.js +85 -0
- package/schemas/mutations/analytics/index.js +1 -0
- package/schemas/session.schema.js +3 -1
- package/src/analytics/context.d.ts +85 -0
- package/src/analytics/dispatcher.d.ts +71 -0
- package/src/analytics/index.d.ts +6 -0
- package/src/analytics/legacy-adapter.d.ts +10 -0
- package/src/analytics/origins.d.ts +25 -0
- package/src/analytics/recording-subscriber.d.ts +18 -0
- package/src/analytics/refs.d.ts +8 -0
- package/src/capabilities/analytics.capability.d.ts +64 -17
- package/src/capabilities/analytics.provider.d.ts +24 -0
- package/src/capabilities/index.d.ts +1 -0
- package/src/client/client-builder.d.ts +25 -2
- package/src/index.d.ts +1 -0
- package/src/schemas/capabilities.schema.d.ts +8 -1
- package/src/schemas/models/analytics/consent.model.d.ts +13 -0
- package/src/schemas/models/analytics/dispatch.model.d.ts +29 -0
- package/src/schemas/models/analytics/envelope.model.d.ts +565 -0
- package/src/schemas/models/analytics/event-parts.model.d.ts +248 -0
- package/src/schemas/models/analytics/event.model.d.ts +83182 -0
- package/src/schemas/models/analytics/index.d.ts +6 -0
- package/src/schemas/models/analytics/origin.model.d.ts +275 -0
- package/src/schemas/models/analytics.model.d.ts +1 -2
- package/src/schemas/models/index.d.ts +1 -0
- package/src/schemas/mutations/analytics/events/account.mutation.d.ts +1939 -0
- package/src/schemas/mutations/analytics/events/base.mutation.d.ts +121 -0
- package/src/schemas/mutations/analytics/events/browse.mutation.d.ts +1832 -0
- package/src/schemas/mutations/analytics/events/commerce.mutation.d.ts +30188 -0
- package/src/schemas/mutations/analytics/events/index.d.ts +33928 -0
- package/src/schemas/mutations/analytics/index.d.ts +1 -0
- package/src/schemas/mutations/checkout.mutation.d.ts +4 -4
- package/src/schemas/mutations/product-list.mutation.d.ts +2 -2
- package/src/schemas/session.schema.d.ts +8 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { error, success } from "../schemas/result.js";
|
|
2
|
+
const DEFAULT_CONCURRENCY = 5;
|
|
3
|
+
function notConfigured(name) {
|
|
4
|
+
return error({ type: "Generic", message: `The ${name} capability is not configured, so analytics cannot look it up` });
|
|
5
|
+
}
|
|
6
|
+
function notFound(identifier) {
|
|
7
|
+
return error({ type: "NotFound", identifier });
|
|
8
|
+
}
|
|
9
|
+
function memoize(fn, keyOf) {
|
|
10
|
+
const cache = /* @__PURE__ */ new Map();
|
|
11
|
+
return (arg) => {
|
|
12
|
+
const key = keyOf(arg);
|
|
13
|
+
const hit = cache.get(key);
|
|
14
|
+
if (hit) {
|
|
15
|
+
return hit;
|
|
16
|
+
}
|
|
17
|
+
const pending = fn(arg);
|
|
18
|
+
cache.set(key, pending);
|
|
19
|
+
return pending;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function memoizeOnce(fn) {
|
|
23
|
+
let pending;
|
|
24
|
+
return () => pending ??= fn();
|
|
25
|
+
}
|
|
26
|
+
async function mapLimit(items, limit, fn) {
|
|
27
|
+
const results = new Array(items.length);
|
|
28
|
+
let next = 0;
|
|
29
|
+
const worker = async () => {
|
|
30
|
+
while (next < items.length) {
|
|
31
|
+
const index = next++;
|
|
32
|
+
results[index] = await fn(items[index]);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
function createAnalyticsContext(dependencies) {
|
|
39
|
+
const { capabilities, requestContext } = dependencies;
|
|
40
|
+
const concurrency = Math.max(1, dependencies.concurrency ?? DEFAULT_CONCURRENCY);
|
|
41
|
+
const productById = memoize(
|
|
42
|
+
async (id) => capabilities.product ? capabilities.product.getById({ identifier: id }) : notConfigured("product"),
|
|
43
|
+
(id) => id.key
|
|
44
|
+
);
|
|
45
|
+
const productBySku = memoize(
|
|
46
|
+
async (variant) => capabilities.product ? capabilities.product.getBySKU({ variant }) : notConfigured("product"),
|
|
47
|
+
(variant) => variant.sku
|
|
48
|
+
);
|
|
49
|
+
const categoryById = memoize(
|
|
50
|
+
async (id) => capabilities.category ? capabilities.category.getById({ id }) : notConfigured("category"),
|
|
51
|
+
(id) => id.key
|
|
52
|
+
);
|
|
53
|
+
const categoryPath = memoize(
|
|
54
|
+
async (id) => capabilities.category ? capabilities.category.getBreadcrumbPathToCategory({ id }) : notConfigured("category"),
|
|
55
|
+
(id) => id.key
|
|
56
|
+
);
|
|
57
|
+
const pricesForSku = memoize(
|
|
58
|
+
async (variant) => {
|
|
59
|
+
if (!capabilities.price) {
|
|
60
|
+
return notConfigured("price");
|
|
61
|
+
}
|
|
62
|
+
const [customer, list] = await Promise.all([
|
|
63
|
+
capabilities.price.getCustomerPrice({ variant }),
|
|
64
|
+
capabilities.price.getListPrice({ variant })
|
|
65
|
+
]);
|
|
66
|
+
if (!customer.success && !list.success) {
|
|
67
|
+
return customer;
|
|
68
|
+
}
|
|
69
|
+
return success({
|
|
70
|
+
customer: customer.success ? customer.value : void 0,
|
|
71
|
+
list: list.success ? list.value : void 0
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
(variant) => variant.sku
|
|
75
|
+
);
|
|
76
|
+
const storeById = memoize(
|
|
77
|
+
async (id) => capabilities.store ? capabilities.store.getById({ identifier: id }) : notConfigured("store"),
|
|
78
|
+
(id) => id.key
|
|
79
|
+
);
|
|
80
|
+
const inventoryForSku = memoize(
|
|
81
|
+
async (variant) => {
|
|
82
|
+
if (!capabilities.inventory) {
|
|
83
|
+
return notConfigured("inventory");
|
|
84
|
+
}
|
|
85
|
+
const store = await storeById(requestContext.storeIdentifier);
|
|
86
|
+
if (!store.success) {
|
|
87
|
+
return store;
|
|
88
|
+
}
|
|
89
|
+
return capabilities.inventory.getBySKU({ variant, fulfilmentCenter: store.value.fulfillmentCenter });
|
|
90
|
+
},
|
|
91
|
+
(variant) => variant.sku
|
|
92
|
+
);
|
|
93
|
+
const activeCart = memoizeOnce(
|
|
94
|
+
async () => {
|
|
95
|
+
if (!capabilities.cart) {
|
|
96
|
+
return notConfigured("cart");
|
|
97
|
+
}
|
|
98
|
+
const id = await capabilities.cart.getActiveCartId();
|
|
99
|
+
if (!id.success) {
|
|
100
|
+
return id;
|
|
101
|
+
}
|
|
102
|
+
return capabilities.cart.getById({ cart: id.value });
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
const orderById = memoize(
|
|
106
|
+
async (id) => capabilities.order ? capabilities.order.getById({ order: id }) : notConfigured("order"),
|
|
107
|
+
(id) => id.key
|
|
108
|
+
);
|
|
109
|
+
const selfIdentity = memoizeOnce(
|
|
110
|
+
async () => capabilities.identity ? capabilities.identity.getSelf({}) : notConfigured("identity")
|
|
111
|
+
);
|
|
112
|
+
const selfProfile = memoizeOnce(
|
|
113
|
+
async () => {
|
|
114
|
+
if (!capabilities.profile) {
|
|
115
|
+
return notConfigured("profile");
|
|
116
|
+
}
|
|
117
|
+
const identity = requestContext.session.identityContext.identity;
|
|
118
|
+
if (identity.type === "Anonymous") {
|
|
119
|
+
return notFound(identity);
|
|
120
|
+
}
|
|
121
|
+
return capabilities.profile.getById({ identifier: identity.id });
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
const selfPersonalization = memoizeOnce(
|
|
125
|
+
async () => {
|
|
126
|
+
if (!capabilities.personalizationProfile) {
|
|
127
|
+
return notConfigured("personalizationProfile");
|
|
128
|
+
}
|
|
129
|
+
const marketing = requestContext.session.marketingContext.identifier;
|
|
130
|
+
return capabilities.personalizationProfile.getPersonalizationProfile({
|
|
131
|
+
identity: requestContext.session.identityContext.identity,
|
|
132
|
+
personalizationProfileIdentifier: marketing.key ? marketing : void 0
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
const selfCompany = memoizeOnce(
|
|
137
|
+
async () => {
|
|
138
|
+
if (!capabilities.company) {
|
|
139
|
+
return notConfigured("company");
|
|
140
|
+
}
|
|
141
|
+
const cart = await activeCart();
|
|
142
|
+
if (!cart.success) {
|
|
143
|
+
return cart;
|
|
144
|
+
}
|
|
145
|
+
if (!cart.value.company) {
|
|
146
|
+
return notFound(cart.value.identifier);
|
|
147
|
+
}
|
|
148
|
+
return capabilities.company.getById({ identifier: cart.value.company });
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
return {
|
|
152
|
+
products: {
|
|
153
|
+
byId: productById,
|
|
154
|
+
bySku: productBySku,
|
|
155
|
+
many: (ids) => mapLimit(ids, concurrency, productById)
|
|
156
|
+
},
|
|
157
|
+
categories: { byId: categoryById, path: categoryPath },
|
|
158
|
+
prices: { forSku: pricesForSku },
|
|
159
|
+
inventory: { forSku: inventoryForSku },
|
|
160
|
+
cart: { active: () => activeCart() },
|
|
161
|
+
orders: { byId: orderById },
|
|
162
|
+
stores: { byId: storeById },
|
|
163
|
+
self: {
|
|
164
|
+
identity: () => selfIdentity(),
|
|
165
|
+
profile: () => selfProfile(),
|
|
166
|
+
personalization: () => selfPersonalization(),
|
|
167
|
+
company: () => selfCompany()
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
createAnalyticsContext
|
|
173
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { traceSpan } from "../decorators/reactionary.decorator.js";
|
|
3
|
+
import {
|
|
4
|
+
AnalyticsEventMutationSchemas,
|
|
5
|
+
AnalyticsEventSchema,
|
|
6
|
+
AnalyticsMutationConsentChangedSchema,
|
|
7
|
+
AnalyticsSubscriberDescriptorSchema
|
|
8
|
+
} from "../schemas/index.js";
|
|
9
|
+
import { error, success } from "../schemas/result.js";
|
|
10
|
+
const DEFAULT_CONSENT = { analytics: true, marketing: true };
|
|
11
|
+
const insideHandler = new AsyncLocalStorage();
|
|
12
|
+
function eventTypeMatches(pattern, type) {
|
|
13
|
+
if (pattern === "*") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (pattern.endsWith("*")) {
|
|
17
|
+
return type.startsWith(pattern.slice(0, -1));
|
|
18
|
+
}
|
|
19
|
+
return pattern === type;
|
|
20
|
+
}
|
|
21
|
+
function isPurposeAllowed(purpose, consent) {
|
|
22
|
+
if (purpose === "necessary") {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return purpose === "analytics" ? consent.analytics : consent.marketing;
|
|
26
|
+
}
|
|
27
|
+
class AnalyticsDispatcher {
|
|
28
|
+
requestContext;
|
|
29
|
+
subscribers;
|
|
30
|
+
createContext;
|
|
31
|
+
options;
|
|
32
|
+
defaultConsent;
|
|
33
|
+
queue = [];
|
|
34
|
+
flushScheduled = false;
|
|
35
|
+
tails = /* @__PURE__ */ new Map();
|
|
36
|
+
inflight = /* @__PURE__ */ new Set();
|
|
37
|
+
constructor(dependencies) {
|
|
38
|
+
this.requestContext = dependencies.requestContext;
|
|
39
|
+
this.createContext = dependencies.createContext;
|
|
40
|
+
this.options = dependencies.options ?? {};
|
|
41
|
+
this.defaultConsent = this.options.defaultConsent ?? DEFAULT_CONSENT;
|
|
42
|
+
const names = /* @__PURE__ */ new Set();
|
|
43
|
+
for (const subscriber of dependencies.subscribers) {
|
|
44
|
+
AnalyticsSubscriberDescriptorSchema.parse({ name: subscriber.name, types: subscriber.types, purpose: subscriber.purpose });
|
|
45
|
+
if (names.has(subscriber.name)) {
|
|
46
|
+
throw new Error(`Duplicate analytics subscriber name '${subscriber.name}'`);
|
|
47
|
+
}
|
|
48
|
+
names.add(subscriber.name);
|
|
49
|
+
}
|
|
50
|
+
this.subscribers = [...dependencies.subscribers];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validates the input, captures request state, enqueues the event and schedules delivery.
|
|
54
|
+
* Never throws: invalid input and emits made from inside a subscriber are logged and dropped.
|
|
55
|
+
*/
|
|
56
|
+
emit(type, input, options = {}) {
|
|
57
|
+
if (insideHandler.getStore()) {
|
|
58
|
+
console.warn(`Analytics event '${type}' was emitted from inside a subscriber and has been dropped`);
|
|
59
|
+
return error({ type: "InvalidInput", error: "Events cannot be emitted from inside an analytics subscriber" });
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const schema = AnalyticsEventMutationSchemas[type];
|
|
63
|
+
const mutation = schema.safeParse(input);
|
|
64
|
+
if (!mutation.success) {
|
|
65
|
+
const message = mutation.error.message;
|
|
66
|
+
console.warn(`Analytics event '${type}' had invalid input and has been dropped: ${message}`);
|
|
67
|
+
return error({ type: "InvalidInput", error: message });
|
|
68
|
+
}
|
|
69
|
+
if (type === "consent-changed") {
|
|
70
|
+
const change = AnalyticsMutationConsentChangedSchema.parse(input);
|
|
71
|
+
this.requestContext.session.consent = change.consent;
|
|
72
|
+
}
|
|
73
|
+
const { origin, properties, ...payload } = input;
|
|
74
|
+
const event = AnalyticsEventSchema.safeParse({
|
|
75
|
+
id: crypto.randomUUID(),
|
|
76
|
+
type,
|
|
77
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
78
|
+
origin,
|
|
79
|
+
properties,
|
|
80
|
+
context: this.snapshot(options),
|
|
81
|
+
payload
|
|
82
|
+
});
|
|
83
|
+
if (!event.success) {
|
|
84
|
+
console.warn(`Analytics event '${type}' could not be built and has been dropped: ${event.error.message}`);
|
|
85
|
+
return error({ type: "InvalidInput", error: event.error.message });
|
|
86
|
+
}
|
|
87
|
+
this.queue.push(event.data);
|
|
88
|
+
this.scheduleFlush();
|
|
89
|
+
return success({ eventId: event.data.id });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(`Analytics event '${type}' failed to enqueue and has been dropped`, err);
|
|
92
|
+
return error({ type: "InvalidInput", error: err instanceof Error ? err.message : String(err) });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Delivers everything queued so far and resolves once all deliveries, including ones already in flight, are done.
|
|
97
|
+
* For scripts, tests and runtimes without `waitUntil`. Never rejects.
|
|
98
|
+
*/
|
|
99
|
+
async flush() {
|
|
100
|
+
this.deliverQueued();
|
|
101
|
+
await Promise.all([...this.inflight]);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A fresh lookup context, for analytics code that has to resolve something before it can emit (the legacy adapter).
|
|
105
|
+
*/
|
|
106
|
+
lookups() {
|
|
107
|
+
return this.createContext();
|
|
108
|
+
}
|
|
109
|
+
snapshot(options) {
|
|
110
|
+
const { session, languageContext, referrer, userAgent, clientIp, isBot } = this.requestContext;
|
|
111
|
+
return {
|
|
112
|
+
marketingIdentifier: { ...options.marketingIdentifier ?? session.marketingContext.identifier },
|
|
113
|
+
identity: session.identityContext.identity,
|
|
114
|
+
locale: languageContext.locale,
|
|
115
|
+
currency: languageContext.currencyCode,
|
|
116
|
+
consent: session.consent ?? this.defaultConsent,
|
|
117
|
+
page: referrer ? { referrer } : void 0,
|
|
118
|
+
userAgent: userAgent || void 0,
|
|
119
|
+
clientIp: clientIp || void 0,
|
|
120
|
+
isBot
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
scheduleFlush() {
|
|
124
|
+
if (this.flushScheduled) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.flushScheduled = true;
|
|
128
|
+
const run = Promise.resolve().then(() => {
|
|
129
|
+
this.flushScheduled = false;
|
|
130
|
+
return this.flush();
|
|
131
|
+
}).catch((err) => console.error("Analytics flush failed", err));
|
|
132
|
+
try {
|
|
133
|
+
this.options.waitUntil?.(run);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error("Analytics waitUntil failed", err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
deliverQueued() {
|
|
139
|
+
const batch = this.queue;
|
|
140
|
+
if (batch.length === 0) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.queue = [];
|
|
144
|
+
const ctx = this.createContext();
|
|
145
|
+
const deliveries = [];
|
|
146
|
+
for (const subscriber of this.subscribers) {
|
|
147
|
+
const events = batch.filter(
|
|
148
|
+
(event) => subscriber.types.some((pattern) => eventTypeMatches(pattern, event.type)) && isPurposeAllowed(subscriber.purpose, event.context.consent ?? this.defaultConsent)
|
|
149
|
+
);
|
|
150
|
+
if (events.length === 0) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const tail = (this.tails.get(subscriber) ?? Promise.resolve()).then(() => this.runSubscriber(subscriber, events, ctx));
|
|
154
|
+
this.tails.set(subscriber, tail);
|
|
155
|
+
deliveries.push(tail);
|
|
156
|
+
}
|
|
157
|
+
const done = Promise.all(deliveries).then(() => void 0);
|
|
158
|
+
this.inflight.add(done);
|
|
159
|
+
void done.finally(() => this.inflight.delete(done));
|
|
160
|
+
}
|
|
161
|
+
async runSubscriber(subscriber, events, ctx) {
|
|
162
|
+
for (const event of events) {
|
|
163
|
+
try {
|
|
164
|
+
const outcome = await insideHandler.run(
|
|
165
|
+
true,
|
|
166
|
+
() => traceSpan(`AnalyticsSubscriber.${subscriber.name}.handle`, () => subscriber.handle(event, ctx))
|
|
167
|
+
);
|
|
168
|
+
if (outcome === "rejected") {
|
|
169
|
+
console.warn(`Analytics subscriber '${subscriber.name}' rejected '${event.type}' (${event.id})`);
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error(`Analytics subscriber '${subscriber.name}' failed on '${event.type}' (${event.id})`, err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export {
|
|
178
|
+
AnalyticsDispatcher,
|
|
179
|
+
eventTypeMatches,
|
|
180
|
+
isPurposeAllowed
|
|
181
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { error } from "../schemas/result.js";
|
|
2
|
+
import { fromDirect, fromSearch } from "./origins.js";
|
|
3
|
+
async function emitLegacyAnalyticsEvent(event, dispatcher) {
|
|
4
|
+
const identity = event.personalizationProfile ? { marketingIdentifier: event.personalizationProfile.identifier } : {};
|
|
5
|
+
switch (event.event) {
|
|
6
|
+
case "product-summary-view":
|
|
7
|
+
return event.source ? dispatcher.emit("search-results-viewed", { search: event.source.identifier, products: event.products }, identity) : dispatcher.emit("product-carousel-viewed", { carousel: { carouselName: "unknown" }, products: event.products }, identity);
|
|
8
|
+
case "product-summary-click":
|
|
9
|
+
return dispatcher.emit(
|
|
10
|
+
"product-clicked",
|
|
11
|
+
{
|
|
12
|
+
product: event.product,
|
|
13
|
+
origin: event.source ? fromSearch(event.source.identifier, event.position) : fromDirect()
|
|
14
|
+
},
|
|
15
|
+
identity
|
|
16
|
+
);
|
|
17
|
+
case "product-details-view":
|
|
18
|
+
return dispatcher.emit("product-viewed", { product: event.product }, identity);
|
|
19
|
+
case "product-cart-add": {
|
|
20
|
+
const lookups = dispatcher.lookups();
|
|
21
|
+
const [cart, product] = await Promise.all([lookups.cart.active(), lookups.products.byId(event.product)]);
|
|
22
|
+
if (!cart.success) {
|
|
23
|
+
return cart;
|
|
24
|
+
}
|
|
25
|
+
if (!product.success) {
|
|
26
|
+
return product;
|
|
27
|
+
}
|
|
28
|
+
return dispatcher.emit(
|
|
29
|
+
"cart-item-added",
|
|
30
|
+
{
|
|
31
|
+
cart: cart.value,
|
|
32
|
+
variant: { sku: product.value.mainVariant.identifier.sku },
|
|
33
|
+
quantity: 1,
|
|
34
|
+
// The legacy source carries no position, only the search that led here.
|
|
35
|
+
origin: event.source ? fromSearch(event.source.identifier, 0) : void 0
|
|
36
|
+
},
|
|
37
|
+
identity
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
case "purchase":
|
|
41
|
+
return dispatcher.emit("order-placed", { order: event.order }, identity);
|
|
42
|
+
default:
|
|
43
|
+
return error({ type: "Generic", message: "Unsupported legacy analytics event" });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
emitLegacyAnalyticsEvent
|
|
48
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function fromSearch(identifier, position) {
|
|
2
|
+
return { type: "search", identifier, position };
|
|
3
|
+
}
|
|
4
|
+
function fromAutocomplete(term, position) {
|
|
5
|
+
return { type: "autocomplete", term, position };
|
|
6
|
+
}
|
|
7
|
+
function fromCategory(categoryId, filters, position) {
|
|
8
|
+
return { type: "category", categoryId, filters, position };
|
|
9
|
+
}
|
|
10
|
+
function fromList(recommendationId, carouselName, position) {
|
|
11
|
+
return { type: "list", recommendationId, carouselName, position };
|
|
12
|
+
}
|
|
13
|
+
function fromAssociations(productId, kind, position) {
|
|
14
|
+
return { type: "associations", productId, kind, position };
|
|
15
|
+
}
|
|
16
|
+
function fromProductList(listId, position) {
|
|
17
|
+
return { type: "product-list", listId, position };
|
|
18
|
+
}
|
|
19
|
+
function fromPromotion(promotionId, slot) {
|
|
20
|
+
return { type: "promotion", promotionId, slot };
|
|
21
|
+
}
|
|
22
|
+
function fromContent(contentId, slot) {
|
|
23
|
+
return { type: "content", contentId, slot };
|
|
24
|
+
}
|
|
25
|
+
function fromCart(kind) {
|
|
26
|
+
return { type: "cart", kind };
|
|
27
|
+
}
|
|
28
|
+
function fromOrder(orderId) {
|
|
29
|
+
return { type: "order", orderId };
|
|
30
|
+
}
|
|
31
|
+
function fromQuickOrder(kind) {
|
|
32
|
+
return { type: "quick-order", kind };
|
|
33
|
+
}
|
|
34
|
+
function fromPDP(upstream) {
|
|
35
|
+
return { type: "pdp", upstream };
|
|
36
|
+
}
|
|
37
|
+
function fromDirect() {
|
|
38
|
+
return { type: "direct" };
|
|
39
|
+
}
|
|
40
|
+
function searchOriginOf(origin) {
|
|
41
|
+
if (origin?.type === "search") {
|
|
42
|
+
return origin;
|
|
43
|
+
}
|
|
44
|
+
if (origin?.type === "pdp") {
|
|
45
|
+
return searchOriginOf(origin.upstream);
|
|
46
|
+
}
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
fromAssociations,
|
|
51
|
+
fromAutocomplete,
|
|
52
|
+
fromCart,
|
|
53
|
+
fromCategory,
|
|
54
|
+
fromContent,
|
|
55
|
+
fromDirect,
|
|
56
|
+
fromList,
|
|
57
|
+
fromOrder,
|
|
58
|
+
fromPDP,
|
|
59
|
+
fromProductList,
|
|
60
|
+
fromPromotion,
|
|
61
|
+
fromQuickOrder,
|
|
62
|
+
fromSearch,
|
|
63
|
+
searchOriginOf
|
|
64
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class RecordingAnalyticsSubscriber {
|
|
2
|
+
constructor(name = "recording", types = ["*"], purpose = "necessary") {
|
|
3
|
+
this.name = name;
|
|
4
|
+
this.types = types;
|
|
5
|
+
this.purpose = purpose;
|
|
6
|
+
}
|
|
7
|
+
name;
|
|
8
|
+
types;
|
|
9
|
+
purpose;
|
|
10
|
+
events = [];
|
|
11
|
+
/** Lookup contexts the subscriber was handed, one per flush. */
|
|
12
|
+
contexts = [];
|
|
13
|
+
async handle(event, ctx) {
|
|
14
|
+
this.events.push(event);
|
|
15
|
+
if (!this.contexts.includes(ctx)) {
|
|
16
|
+
this.contexts.push(ctx);
|
|
17
|
+
}
|
|
18
|
+
return "accepted";
|
|
19
|
+
}
|
|
20
|
+
eventsOfType(type) {
|
|
21
|
+
return this.events.filter((event) => event.type === type);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
RecordingAnalyticsSubscriber
|
|
26
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function productKey(ref) {
|
|
2
|
+
return "key" in ref ? ref.key : ref.identifier.key;
|
|
3
|
+
}
|
|
4
|
+
function categoryKey(ref) {
|
|
5
|
+
return "key" in ref ? ref.key : ref.identifier.key;
|
|
6
|
+
}
|
|
7
|
+
function storeKey(ref) {
|
|
8
|
+
return "key" in ref ? ref.key : ref.identifier.key;
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
categoryKey,
|
|
12
|
+
productKey,
|
|
13
|
+
storeKey
|
|
14
|
+
};
|