@abconvert/headless 0.0.0-stage → 0.1.1
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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +41 -2
- package/dist/hydrogen-analytics-provider.cjs +1 -0
- package/dist/hydrogen-analytics-provider.d.cts +12 -0
- package/dist/hydrogen-analytics-provider.d.ts +12 -0
- package/dist/hydrogen-analytics-provider.js +1 -0
- package/dist/hydrogen.cjs +1 -0
- package/dist/hydrogen.d.cts +12 -0
- package/dist/hydrogen.d.ts +12 -0
- package/dist/hydrogen.js +1 -0
- package/dist/index.cjs +8 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/next/types.d.cts +35 -0
- package/dist/next/types.d.ts +35 -0
- package/dist/next.cjs +8 -0
- package/dist/next.d.cts +9 -0
- package/dist/next.d.ts +9 -0
- package/dist/next.js +8 -0
- package/dist/react.cjs +2 -0
- package/dist/react.d.cts +29 -0
- package/dist/react.d.ts +29 -0
- package/dist/react.js +2 -0
- package/dist/types.d.cts +302 -0
- package/dist/types.d.ts +302 -0
- package/package.json +74 -4
package/dist/types.d.cts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
export type AssignmentType = 'price' | 'shipping' | 'theme' | 'template' | 'url_redirect' | 'checkout' | 'offer' | 'visual_editor' | 'combined' | 'personalization';
|
|
2
|
+
export type AssignmentReason = 'random_split' | 'force_assign_group' | 'preview_override' | 'url_force_assign';
|
|
3
|
+
/** Why a visitor was left out of a test, for the exclusions reported as test group -1. */
|
|
4
|
+
export type ReportedExclusion = 'audience_targeting_excluded' | 'force_assign_excluded';
|
|
5
|
+
/** What an assignment row names as its reason: a test group the visitor counts in, or an exclusion. */
|
|
6
|
+
export type ReportedReason = 'random_split' | 'force_assign_group' | ReportedExclusion;
|
|
7
|
+
export interface TestGroup {
|
|
8
|
+
index: number;
|
|
9
|
+
name: string;
|
|
10
|
+
control: boolean;
|
|
11
|
+
split: number;
|
|
12
|
+
/** A feature test's values for this test group; `{}` for every other test type. */
|
|
13
|
+
variables: Record<string, string | number | boolean>;
|
|
14
|
+
}
|
|
15
|
+
export interface Assignment {
|
|
16
|
+
experimentId: string;
|
|
17
|
+
experimentName: string;
|
|
18
|
+
type: AssignmentType;
|
|
19
|
+
status: 'active' | 'preview';
|
|
20
|
+
testGroup: TestGroup;
|
|
21
|
+
reason: AssignmentReason;
|
|
22
|
+
/** Feature tests only. */
|
|
23
|
+
featureKey?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Price {
|
|
26
|
+
experimentId: string;
|
|
27
|
+
testGroup: TestGroup;
|
|
28
|
+
amount: number;
|
|
29
|
+
compareAtAmount: number | null;
|
|
30
|
+
currency: string;
|
|
31
|
+
country: string;
|
|
32
|
+
productId: string;
|
|
33
|
+
variantId: string;
|
|
34
|
+
}
|
|
35
|
+
export interface PriceOptions {
|
|
36
|
+
country?: string;
|
|
37
|
+
aggregate?: 'min' | 'max' | 'first';
|
|
38
|
+
}
|
|
39
|
+
export interface ShippingRate {
|
|
40
|
+
experimentId: string;
|
|
41
|
+
testGroup: TestGroup;
|
|
42
|
+
zone: string;
|
|
43
|
+
name: string;
|
|
44
|
+
amount: number;
|
|
45
|
+
currency: string;
|
|
46
|
+
condition: {
|
|
47
|
+
type: 'price' | 'weight';
|
|
48
|
+
minimum: number | null;
|
|
49
|
+
maximum: number | null;
|
|
50
|
+
unit: string | null;
|
|
51
|
+
} | null;
|
|
52
|
+
}
|
|
53
|
+
export interface FreeShippingThreshold {
|
|
54
|
+
experimentId: string;
|
|
55
|
+
testGroup: TestGroup;
|
|
56
|
+
zone: string;
|
|
57
|
+
amount: number;
|
|
58
|
+
currency: string;
|
|
59
|
+
}
|
|
60
|
+
export interface ShippingOptions {
|
|
61
|
+
zone?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface Offer {
|
|
64
|
+
experimentId: string;
|
|
65
|
+
testGroup: TestGroup;
|
|
66
|
+
title: string;
|
|
67
|
+
discounts: unknown[];
|
|
68
|
+
}
|
|
69
|
+
export interface CartAttribute {
|
|
70
|
+
key: string;
|
|
71
|
+
value: string;
|
|
72
|
+
}
|
|
73
|
+
export interface CheckoutLine {
|
|
74
|
+
/** A numeric product variant ID, or a Storefront API `gid://shopify/ProductVariant/<id>`. */
|
|
75
|
+
variantId: string | number;
|
|
76
|
+
quantity: number;
|
|
77
|
+
}
|
|
78
|
+
export interface CheckoutUrlOptions {
|
|
79
|
+
lines: CheckoutLine[];
|
|
80
|
+
/** Opens Shop Pay checkout. */
|
|
81
|
+
payment?: 'shop_pay';
|
|
82
|
+
}
|
|
83
|
+
export interface Customer {
|
|
84
|
+
loggedIn?: boolean;
|
|
85
|
+
tags?: string[];
|
|
86
|
+
ordersCount?: number;
|
|
87
|
+
/** In your store currency. */
|
|
88
|
+
totalSpent?: number;
|
|
89
|
+
}
|
|
90
|
+
/** Anything with headers and a URL: a Fetch `Request`, or `{ headers, url }`. */
|
|
91
|
+
export interface RequestLike {
|
|
92
|
+
url: string;
|
|
93
|
+
headers: {
|
|
94
|
+
get(name: string): string | null;
|
|
95
|
+
} | Record<string, string | string[] | undefined>;
|
|
96
|
+
}
|
|
97
|
+
export interface ABConvertOptions {
|
|
98
|
+
/** Your `myshopify.com` domain. */
|
|
99
|
+
shop: string;
|
|
100
|
+
/** A storefront key from Settings, Headless. Safe to ship in storefront code. */
|
|
101
|
+
storefrontKey: string;
|
|
102
|
+
/** The incoming request. Required on the server. */
|
|
103
|
+
request?: RequestLike;
|
|
104
|
+
/** The country your storefront shows prices for. Defaults to the store's default country. */
|
|
105
|
+
country?: string;
|
|
106
|
+
customer?: Customer;
|
|
107
|
+
/** Milliseconds to wait for your tests on a cold start. Defaults to 1000. */
|
|
108
|
+
timeout?: number;
|
|
109
|
+
/** Seconds a loaded copy of your tests stays fresh. Defaults to 60. */
|
|
110
|
+
cacheTtl?: number;
|
|
111
|
+
onError?: (error: Error) => void;
|
|
112
|
+
consent?: {
|
|
113
|
+
analytics: boolean;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Your storefront loads Shopify's Customer Privacy API, which writes the
|
|
117
|
+
* `_tracking_consent` cookie once the visitor answers: until then, track
|
|
118
|
+
* nothing. The Hydrogen entries' `createABConvert` sets it for you.
|
|
119
|
+
*/
|
|
120
|
+
expectCustomerPrivacy?: boolean;
|
|
121
|
+
/** Where ABConvert runs. For ABConvert's own testing; leave unset. */
|
|
122
|
+
baseUrl?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Build the client from a page key (`getPageKey()`, or the segment the
|
|
125
|
+
* `/next` middleware rewrites to) instead of a request: what a cached page
|
|
126
|
+
* renders with. It reads prices and assignments for the key's test groups
|
|
127
|
+
* and nothing about the visitor. `''` or `-` is a page no test changes.
|
|
128
|
+
* Rejects when the tests the key names cannot be loaded, so a cache never
|
|
129
|
+
* keeps your original store under a test group's key.
|
|
130
|
+
*/
|
|
131
|
+
pageKey?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Your product pages show no price but their own product's (no related or
|
|
134
|
+
* recently viewed products with prices): a product page's key then carries
|
|
135
|
+
* only the price tests that list it, so untested product pages stay
|
|
136
|
+
* cacheable. Leave unset otherwise, or another product's price shows
|
|
137
|
+
* without its test.
|
|
138
|
+
*/
|
|
139
|
+
onlyOwnProductPrices?: boolean;
|
|
140
|
+
}
|
|
141
|
+
/** What `getClientState()` hands the browser so it rebuilds the same client without a fetch. */
|
|
142
|
+
export interface ClientState {
|
|
143
|
+
version: 1;
|
|
144
|
+
shop: string;
|
|
145
|
+
visitorId: string;
|
|
146
|
+
sessionId: string;
|
|
147
|
+
country: string;
|
|
148
|
+
consent: {
|
|
149
|
+
analytics: boolean;
|
|
150
|
+
};
|
|
151
|
+
preview: boolean;
|
|
152
|
+
assignments: Assignment[];
|
|
153
|
+
/** The document, cut to each assigned test and its assigned test group's changes. */
|
|
154
|
+
doc: unknown;
|
|
155
|
+
pending: {
|
|
156
|
+
assignments: PendingAssignment[];
|
|
157
|
+
exposures: string[];
|
|
158
|
+
};
|
|
159
|
+
/** The locale the server formatted prices in. The browser formats in it too, so hydration matches. */
|
|
160
|
+
locale: string;
|
|
161
|
+
/** The `abconvert-state` cookie value, which the browser writes when tracking starts. */
|
|
162
|
+
stateCookie: string;
|
|
163
|
+
/** Where the server loaded the tests from, when not ABConvert's default. The browser tracks to the same place. */
|
|
164
|
+
baseUrl?: string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* What a page rendered from a page key hands the browser (`getPageState()`).
|
|
168
|
+
* The same for everyone in the same test groups, so it may be cached: no
|
|
169
|
+
* visitor, session, consent or pending work. The browser reads those from its
|
|
170
|
+
* cookies.
|
|
171
|
+
*/
|
|
172
|
+
export interface PageState {
|
|
173
|
+
version: 2;
|
|
174
|
+
shop: string;
|
|
175
|
+
pageKey: string;
|
|
176
|
+
country: string;
|
|
177
|
+
/** The key's assignments: what the page renders for. */
|
|
178
|
+
assignments: Assignment[];
|
|
179
|
+
/** The keyed tests cut to their test group's changes, and every other test without prices or audience rules. */
|
|
180
|
+
doc: unknown;
|
|
181
|
+
locale: string;
|
|
182
|
+
baseUrl?: string;
|
|
183
|
+
/**
|
|
184
|
+
* For the browser to fetch the current tests when its visitor is in one this
|
|
185
|
+
* copy predates. Safe in storefront code. Absent on a page cached
|
|
186
|
+
* before it was added.
|
|
187
|
+
*/
|
|
188
|
+
storefrontKey?: string;
|
|
189
|
+
}
|
|
190
|
+
export interface PendingAssignment {
|
|
191
|
+
experimentId: string;
|
|
192
|
+
objectId: string;
|
|
193
|
+
/** -1 for an excluded visitor. */
|
|
194
|
+
groupIndex: number;
|
|
195
|
+
reason: ReportedReason;
|
|
196
|
+
/** The test's assignment version the decision was made under, when it has one. */
|
|
197
|
+
assignmentVersion?: number;
|
|
198
|
+
assignedAt: string;
|
|
199
|
+
snapshot: {
|
|
200
|
+
country?: string;
|
|
201
|
+
device?: string;
|
|
202
|
+
visitorType?: string;
|
|
203
|
+
trafficPlatform?: string;
|
|
204
|
+
trafficChannel?: string;
|
|
205
|
+
referralDomain?: string;
|
|
206
|
+
landingPage?: string;
|
|
207
|
+
browser?: {
|
|
208
|
+
name: string;
|
|
209
|
+
version: string;
|
|
210
|
+
};
|
|
211
|
+
os?: {
|
|
212
|
+
name: string;
|
|
213
|
+
version: string;
|
|
214
|
+
};
|
|
215
|
+
locale?: string;
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export interface ABConvert {
|
|
219
|
+
getAssignment(experimentId: string | number): Assignment | null;
|
|
220
|
+
getAssignments(): Assignment[];
|
|
221
|
+
getFeature(key: string): Assignment | null;
|
|
222
|
+
getPriceByVariantId(variantId: string | number, options?: PriceOptions): Price | null;
|
|
223
|
+
getPriceByProductId(productId: string | number, options?: PriceOptions): Price | null;
|
|
224
|
+
formatPrice(amount: number, currency?: string): string;
|
|
225
|
+
getShippingRates(options?: ShippingOptions): ShippingRate[] | null;
|
|
226
|
+
getFreeShippingThreshold(options?: ShippingOptions): FreeShippingThreshold | null;
|
|
227
|
+
getOffers(): Offer[];
|
|
228
|
+
getVisitorId(): string;
|
|
229
|
+
getCountry(): string;
|
|
230
|
+
forceTestGroup(experimentId: string | number, index: number): void;
|
|
231
|
+
clearForcedTestGroup(experimentId: string | number): void;
|
|
232
|
+
getCartAttributes(): CartAttribute[];
|
|
233
|
+
getCartLineAttributes(): CartAttribute[];
|
|
234
|
+
/** A cart permalink that lands in checkout with the visitor's test groups. Build it per render; never share or cache it. */
|
|
235
|
+
getCheckoutUrl(options: CheckoutUrlOptions): string;
|
|
236
|
+
/** Server: the visitor's URL redirect destination for `url`, as a same-origin path, or null to stay. Null in the browser. */
|
|
237
|
+
getRedirectUrl(url: string): string | null;
|
|
238
|
+
getClientState(): ClientState;
|
|
239
|
+
/** The page key: the test groups this page renders for, the same for everyone in them. `''` when no test changes the page. */
|
|
240
|
+
getPageKey(): string;
|
|
241
|
+
getSetCookieHeaders(): string[];
|
|
242
|
+
refreshTests(): Promise<void>;
|
|
243
|
+
/** Browser: records `page_viewed`, `product_viewed`, `product_added_to_cart`, or a custom event by its name in ABConvert. Does nothing on the server. */
|
|
244
|
+
track(name: string, payload?: Record<string, unknown>): void;
|
|
245
|
+
/** Browser: records that the visitor saw the test, once per test per session. Does nothing on the server. */
|
|
246
|
+
trackExposure(experimentId: string | number): void;
|
|
247
|
+
/** Browser: the visitor's answer to your cookie banner. Does nothing on the server. */
|
|
248
|
+
setConsent(consent: {
|
|
249
|
+
analytics: boolean;
|
|
250
|
+
}): void;
|
|
251
|
+
}
|
|
252
|
+
/** A client made from a page key: `createABConvert({ pageKey })`. */
|
|
253
|
+
export interface PageABConvert extends ABConvert {
|
|
254
|
+
/** What the browser rebuilds the same reads from. Cacheable: nothing about the visitor. */
|
|
255
|
+
getPageState(): PageState;
|
|
256
|
+
}
|
|
257
|
+
/** A client made from a request on the server: `createABConvert({ request })`. */
|
|
258
|
+
export interface ServerABConvert extends ABConvert {
|
|
259
|
+
/**
|
|
260
|
+
* Whether a test can change this page for some visitor, whatever this
|
|
261
|
+
* visitor's groups: then no shared cache may keep one copy of it for everyone.
|
|
262
|
+
* True when a price test prices a product this URL shows or a URL redirect
|
|
263
|
+
* test covers it, when a read on this client returned a test's data (a
|
|
264
|
+
* tested product's price, offers or shipping rates while such a test runs,
|
|
265
|
+
* an assignment), when `getClientState()` was read, and in a preview session.
|
|
266
|
+
*/
|
|
267
|
+
isPageTested(): boolean;
|
|
268
|
+
/**
|
|
269
|
+
* What the browser needs from this request, with nothing about the visitor,
|
|
270
|
+
* so a cached page can carry it: the tests this client's reads touched, as
|
|
271
|
+
* the visitor saw them, and every other test light. On Hydrogen, return it
|
|
272
|
+
* from the route loader that read the prices the route renders, and hand it
|
|
273
|
+
* to the provider; the browser reads the visitor from the cookies.
|
|
274
|
+
*/
|
|
275
|
+
getPageState(): PageState;
|
|
276
|
+
/**
|
|
277
|
+
* Whether any test runs for your store: an active test, or a preview test in
|
|
278
|
+
* a preview session. On a framework that caches whole responses, any page
|
|
279
|
+
* could then show one visitor's offer, shipping bar or price.
|
|
280
|
+
*/
|
|
281
|
+
isTesting(): boolean;
|
|
282
|
+
}
|
|
283
|
+
/** The client in the browser, from `hydrateABConvert` or a browser-only `createABConvert`: the same methods, plus what a binding needs to run it. */
|
|
284
|
+
export interface BrowserABConvert extends ABConvert {
|
|
285
|
+
/** Begin tracking: report what is pending once consent allows. Idempotent. */
|
|
286
|
+
start(): void;
|
|
287
|
+
/** Stop listening for the visitor's consent. What is held stays held. */
|
|
288
|
+
stop(): void;
|
|
289
|
+
/** Called after a force changes what the reads answer. Returns the undo. */
|
|
290
|
+
subscribe(listener: () => void): () => void;
|
|
291
|
+
}
|
|
292
|
+
export interface HydrateOptions {
|
|
293
|
+
/** Runs once per assignment per page load, once the visitor allows analytics. Not for forced or preview assignments. */
|
|
294
|
+
onAssignment?: (assignment: Assignment) => void;
|
|
295
|
+
/**
|
|
296
|
+
* Hold all tracking, cookies included, until Shopify's Customer Privacy API
|
|
297
|
+
* loads and allows analytics, as Hydrogen does. Set it when your storefront
|
|
298
|
+
* loads that API; the Hydrogen entries set it for you. Without it, a page
|
|
299
|
+
* where the API is absent follows `setConsent` or the server's view.
|
|
300
|
+
*/
|
|
301
|
+
expectCustomerPrivacy?: boolean;
|
|
302
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
export type AssignmentType = 'price' | 'shipping' | 'theme' | 'template' | 'url_redirect' | 'checkout' | 'offer' | 'visual_editor' | 'combined' | 'personalization';
|
|
2
|
+
export type AssignmentReason = 'random_split' | 'force_assign_group' | 'preview_override' | 'url_force_assign';
|
|
3
|
+
/** Why a visitor was left out of a test, for the exclusions reported as test group -1. */
|
|
4
|
+
export type ReportedExclusion = 'audience_targeting_excluded' | 'force_assign_excluded';
|
|
5
|
+
/** What an assignment row names as its reason: a test group the visitor counts in, or an exclusion. */
|
|
6
|
+
export type ReportedReason = 'random_split' | 'force_assign_group' | ReportedExclusion;
|
|
7
|
+
export interface TestGroup {
|
|
8
|
+
index: number;
|
|
9
|
+
name: string;
|
|
10
|
+
control: boolean;
|
|
11
|
+
split: number;
|
|
12
|
+
/** A feature test's values for this test group; `{}` for every other test type. */
|
|
13
|
+
variables: Record<string, string | number | boolean>;
|
|
14
|
+
}
|
|
15
|
+
export interface Assignment {
|
|
16
|
+
experimentId: string;
|
|
17
|
+
experimentName: string;
|
|
18
|
+
type: AssignmentType;
|
|
19
|
+
status: 'active' | 'preview';
|
|
20
|
+
testGroup: TestGroup;
|
|
21
|
+
reason: AssignmentReason;
|
|
22
|
+
/** Feature tests only. */
|
|
23
|
+
featureKey?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Price {
|
|
26
|
+
experimentId: string;
|
|
27
|
+
testGroup: TestGroup;
|
|
28
|
+
amount: number;
|
|
29
|
+
compareAtAmount: number | null;
|
|
30
|
+
currency: string;
|
|
31
|
+
country: string;
|
|
32
|
+
productId: string;
|
|
33
|
+
variantId: string;
|
|
34
|
+
}
|
|
35
|
+
export interface PriceOptions {
|
|
36
|
+
country?: string;
|
|
37
|
+
aggregate?: 'min' | 'max' | 'first';
|
|
38
|
+
}
|
|
39
|
+
export interface ShippingRate {
|
|
40
|
+
experimentId: string;
|
|
41
|
+
testGroup: TestGroup;
|
|
42
|
+
zone: string;
|
|
43
|
+
name: string;
|
|
44
|
+
amount: number;
|
|
45
|
+
currency: string;
|
|
46
|
+
condition: {
|
|
47
|
+
type: 'price' | 'weight';
|
|
48
|
+
minimum: number | null;
|
|
49
|
+
maximum: number | null;
|
|
50
|
+
unit: string | null;
|
|
51
|
+
} | null;
|
|
52
|
+
}
|
|
53
|
+
export interface FreeShippingThreshold {
|
|
54
|
+
experimentId: string;
|
|
55
|
+
testGroup: TestGroup;
|
|
56
|
+
zone: string;
|
|
57
|
+
amount: number;
|
|
58
|
+
currency: string;
|
|
59
|
+
}
|
|
60
|
+
export interface ShippingOptions {
|
|
61
|
+
zone?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface Offer {
|
|
64
|
+
experimentId: string;
|
|
65
|
+
testGroup: TestGroup;
|
|
66
|
+
title: string;
|
|
67
|
+
discounts: unknown[];
|
|
68
|
+
}
|
|
69
|
+
export interface CartAttribute {
|
|
70
|
+
key: string;
|
|
71
|
+
value: string;
|
|
72
|
+
}
|
|
73
|
+
export interface CheckoutLine {
|
|
74
|
+
/** A numeric product variant ID, or a Storefront API `gid://shopify/ProductVariant/<id>`. */
|
|
75
|
+
variantId: string | number;
|
|
76
|
+
quantity: number;
|
|
77
|
+
}
|
|
78
|
+
export interface CheckoutUrlOptions {
|
|
79
|
+
lines: CheckoutLine[];
|
|
80
|
+
/** Opens Shop Pay checkout. */
|
|
81
|
+
payment?: 'shop_pay';
|
|
82
|
+
}
|
|
83
|
+
export interface Customer {
|
|
84
|
+
loggedIn?: boolean;
|
|
85
|
+
tags?: string[];
|
|
86
|
+
ordersCount?: number;
|
|
87
|
+
/** In your store currency. */
|
|
88
|
+
totalSpent?: number;
|
|
89
|
+
}
|
|
90
|
+
/** Anything with headers and a URL: a Fetch `Request`, or `{ headers, url }`. */
|
|
91
|
+
export interface RequestLike {
|
|
92
|
+
url: string;
|
|
93
|
+
headers: {
|
|
94
|
+
get(name: string): string | null;
|
|
95
|
+
} | Record<string, string | string[] | undefined>;
|
|
96
|
+
}
|
|
97
|
+
export interface ABConvertOptions {
|
|
98
|
+
/** Your `myshopify.com` domain. */
|
|
99
|
+
shop: string;
|
|
100
|
+
/** A storefront key from Settings, Headless. Safe to ship in storefront code. */
|
|
101
|
+
storefrontKey: string;
|
|
102
|
+
/** The incoming request. Required on the server. */
|
|
103
|
+
request?: RequestLike;
|
|
104
|
+
/** The country your storefront shows prices for. Defaults to the store's default country. */
|
|
105
|
+
country?: string;
|
|
106
|
+
customer?: Customer;
|
|
107
|
+
/** Milliseconds to wait for your tests on a cold start. Defaults to 1000. */
|
|
108
|
+
timeout?: number;
|
|
109
|
+
/** Seconds a loaded copy of your tests stays fresh. Defaults to 60. */
|
|
110
|
+
cacheTtl?: number;
|
|
111
|
+
onError?: (error: Error) => void;
|
|
112
|
+
consent?: {
|
|
113
|
+
analytics: boolean;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Your storefront loads Shopify's Customer Privacy API, which writes the
|
|
117
|
+
* `_tracking_consent` cookie once the visitor answers: until then, track
|
|
118
|
+
* nothing. The Hydrogen entries' `createABConvert` sets it for you.
|
|
119
|
+
*/
|
|
120
|
+
expectCustomerPrivacy?: boolean;
|
|
121
|
+
/** Where ABConvert runs. For ABConvert's own testing; leave unset. */
|
|
122
|
+
baseUrl?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Build the client from a page key (`getPageKey()`, or the segment the
|
|
125
|
+
* `/next` middleware rewrites to) instead of a request: what a cached page
|
|
126
|
+
* renders with. It reads prices and assignments for the key's test groups
|
|
127
|
+
* and nothing about the visitor. `''` or `-` is a page no test changes.
|
|
128
|
+
* Rejects when the tests the key names cannot be loaded, so a cache never
|
|
129
|
+
* keeps your original store under a test group's key.
|
|
130
|
+
*/
|
|
131
|
+
pageKey?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Your product pages show no price but their own product's (no related or
|
|
134
|
+
* recently viewed products with prices): a product page's key then carries
|
|
135
|
+
* only the price tests that list it, so untested product pages stay
|
|
136
|
+
* cacheable. Leave unset otherwise, or another product's price shows
|
|
137
|
+
* without its test.
|
|
138
|
+
*/
|
|
139
|
+
onlyOwnProductPrices?: boolean;
|
|
140
|
+
}
|
|
141
|
+
/** What `getClientState()` hands the browser so it rebuilds the same client without a fetch. */
|
|
142
|
+
export interface ClientState {
|
|
143
|
+
version: 1;
|
|
144
|
+
shop: string;
|
|
145
|
+
visitorId: string;
|
|
146
|
+
sessionId: string;
|
|
147
|
+
country: string;
|
|
148
|
+
consent: {
|
|
149
|
+
analytics: boolean;
|
|
150
|
+
};
|
|
151
|
+
preview: boolean;
|
|
152
|
+
assignments: Assignment[];
|
|
153
|
+
/** The document, cut to each assigned test and its assigned test group's changes. */
|
|
154
|
+
doc: unknown;
|
|
155
|
+
pending: {
|
|
156
|
+
assignments: PendingAssignment[];
|
|
157
|
+
exposures: string[];
|
|
158
|
+
};
|
|
159
|
+
/** The locale the server formatted prices in. The browser formats in it too, so hydration matches. */
|
|
160
|
+
locale: string;
|
|
161
|
+
/** The `abconvert-state` cookie value, which the browser writes when tracking starts. */
|
|
162
|
+
stateCookie: string;
|
|
163
|
+
/** Where the server loaded the tests from, when not ABConvert's default. The browser tracks to the same place. */
|
|
164
|
+
baseUrl?: string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* What a page rendered from a page key hands the browser (`getPageState()`).
|
|
168
|
+
* The same for everyone in the same test groups, so it may be cached: no
|
|
169
|
+
* visitor, session, consent or pending work. The browser reads those from its
|
|
170
|
+
* cookies.
|
|
171
|
+
*/
|
|
172
|
+
export interface PageState {
|
|
173
|
+
version: 2;
|
|
174
|
+
shop: string;
|
|
175
|
+
pageKey: string;
|
|
176
|
+
country: string;
|
|
177
|
+
/** The key's assignments: what the page renders for. */
|
|
178
|
+
assignments: Assignment[];
|
|
179
|
+
/** The keyed tests cut to their test group's changes, and every other test without prices or audience rules. */
|
|
180
|
+
doc: unknown;
|
|
181
|
+
locale: string;
|
|
182
|
+
baseUrl?: string;
|
|
183
|
+
/**
|
|
184
|
+
* For the browser to fetch the current tests when its visitor is in one this
|
|
185
|
+
* copy predates. Safe in storefront code. Absent on a page cached
|
|
186
|
+
* before it was added.
|
|
187
|
+
*/
|
|
188
|
+
storefrontKey?: string;
|
|
189
|
+
}
|
|
190
|
+
export interface PendingAssignment {
|
|
191
|
+
experimentId: string;
|
|
192
|
+
objectId: string;
|
|
193
|
+
/** -1 for an excluded visitor. */
|
|
194
|
+
groupIndex: number;
|
|
195
|
+
reason: ReportedReason;
|
|
196
|
+
/** The test's assignment version the decision was made under, when it has one. */
|
|
197
|
+
assignmentVersion?: number;
|
|
198
|
+
assignedAt: string;
|
|
199
|
+
snapshot: {
|
|
200
|
+
country?: string;
|
|
201
|
+
device?: string;
|
|
202
|
+
visitorType?: string;
|
|
203
|
+
trafficPlatform?: string;
|
|
204
|
+
trafficChannel?: string;
|
|
205
|
+
referralDomain?: string;
|
|
206
|
+
landingPage?: string;
|
|
207
|
+
browser?: {
|
|
208
|
+
name: string;
|
|
209
|
+
version: string;
|
|
210
|
+
};
|
|
211
|
+
os?: {
|
|
212
|
+
name: string;
|
|
213
|
+
version: string;
|
|
214
|
+
};
|
|
215
|
+
locale?: string;
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export interface ABConvert {
|
|
219
|
+
getAssignment(experimentId: string | number): Assignment | null;
|
|
220
|
+
getAssignments(): Assignment[];
|
|
221
|
+
getFeature(key: string): Assignment | null;
|
|
222
|
+
getPriceByVariantId(variantId: string | number, options?: PriceOptions): Price | null;
|
|
223
|
+
getPriceByProductId(productId: string | number, options?: PriceOptions): Price | null;
|
|
224
|
+
formatPrice(amount: number, currency?: string): string;
|
|
225
|
+
getShippingRates(options?: ShippingOptions): ShippingRate[] | null;
|
|
226
|
+
getFreeShippingThreshold(options?: ShippingOptions): FreeShippingThreshold | null;
|
|
227
|
+
getOffers(): Offer[];
|
|
228
|
+
getVisitorId(): string;
|
|
229
|
+
getCountry(): string;
|
|
230
|
+
forceTestGroup(experimentId: string | number, index: number): void;
|
|
231
|
+
clearForcedTestGroup(experimentId: string | number): void;
|
|
232
|
+
getCartAttributes(): CartAttribute[];
|
|
233
|
+
getCartLineAttributes(): CartAttribute[];
|
|
234
|
+
/** A cart permalink that lands in checkout with the visitor's test groups. Build it per render; never share or cache it. */
|
|
235
|
+
getCheckoutUrl(options: CheckoutUrlOptions): string;
|
|
236
|
+
/** Server: the visitor's URL redirect destination for `url`, as a same-origin path, or null to stay. Null in the browser. */
|
|
237
|
+
getRedirectUrl(url: string): string | null;
|
|
238
|
+
getClientState(): ClientState;
|
|
239
|
+
/** The page key: the test groups this page renders for, the same for everyone in them. `''` when no test changes the page. */
|
|
240
|
+
getPageKey(): string;
|
|
241
|
+
getSetCookieHeaders(): string[];
|
|
242
|
+
refreshTests(): Promise<void>;
|
|
243
|
+
/** Browser: records `page_viewed`, `product_viewed`, `product_added_to_cart`, or a custom event by its name in ABConvert. Does nothing on the server. */
|
|
244
|
+
track(name: string, payload?: Record<string, unknown>): void;
|
|
245
|
+
/** Browser: records that the visitor saw the test, once per test per session. Does nothing on the server. */
|
|
246
|
+
trackExposure(experimentId: string | number): void;
|
|
247
|
+
/** Browser: the visitor's answer to your cookie banner. Does nothing on the server. */
|
|
248
|
+
setConsent(consent: {
|
|
249
|
+
analytics: boolean;
|
|
250
|
+
}): void;
|
|
251
|
+
}
|
|
252
|
+
/** A client made from a page key: `createABConvert({ pageKey })`. */
|
|
253
|
+
export interface PageABConvert extends ABConvert {
|
|
254
|
+
/** What the browser rebuilds the same reads from. Cacheable: nothing about the visitor. */
|
|
255
|
+
getPageState(): PageState;
|
|
256
|
+
}
|
|
257
|
+
/** A client made from a request on the server: `createABConvert({ request })`. */
|
|
258
|
+
export interface ServerABConvert extends ABConvert {
|
|
259
|
+
/**
|
|
260
|
+
* Whether a test can change this page for some visitor, whatever this
|
|
261
|
+
* visitor's groups: then no shared cache may keep one copy of it for everyone.
|
|
262
|
+
* True when a price test prices a product this URL shows or a URL redirect
|
|
263
|
+
* test covers it, when a read on this client returned a test's data (a
|
|
264
|
+
* tested product's price, offers or shipping rates while such a test runs,
|
|
265
|
+
* an assignment), when `getClientState()` was read, and in a preview session.
|
|
266
|
+
*/
|
|
267
|
+
isPageTested(): boolean;
|
|
268
|
+
/**
|
|
269
|
+
* What the browser needs from this request, with nothing about the visitor,
|
|
270
|
+
* so a cached page can carry it: the tests this client's reads touched, as
|
|
271
|
+
* the visitor saw them, and every other test light. On Hydrogen, return it
|
|
272
|
+
* from the route loader that read the prices the route renders, and hand it
|
|
273
|
+
* to the provider; the browser reads the visitor from the cookies.
|
|
274
|
+
*/
|
|
275
|
+
getPageState(): PageState;
|
|
276
|
+
/**
|
|
277
|
+
* Whether any test runs for your store: an active test, or a preview test in
|
|
278
|
+
* a preview session. On a framework that caches whole responses, any page
|
|
279
|
+
* could then show one visitor's offer, shipping bar or price.
|
|
280
|
+
*/
|
|
281
|
+
isTesting(): boolean;
|
|
282
|
+
}
|
|
283
|
+
/** The client in the browser, from `hydrateABConvert` or a browser-only `createABConvert`: the same methods, plus what a binding needs to run it. */
|
|
284
|
+
export interface BrowserABConvert extends ABConvert {
|
|
285
|
+
/** Begin tracking: report what is pending once consent allows. Idempotent. */
|
|
286
|
+
start(): void;
|
|
287
|
+
/** Stop listening for the visitor's consent. What is held stays held. */
|
|
288
|
+
stop(): void;
|
|
289
|
+
/** Called after a force changes what the reads answer. Returns the undo. */
|
|
290
|
+
subscribe(listener: () => void): () => void;
|
|
291
|
+
}
|
|
292
|
+
export interface HydrateOptions {
|
|
293
|
+
/** Runs once per assignment per page load, once the visitor allows analytics. Not for forced or preview assignments. */
|
|
294
|
+
onAssignment?: (assignment: Assignment) => void;
|
|
295
|
+
/**
|
|
296
|
+
* Hold all tracking, cookies included, until Shopify's Customer Privacy API
|
|
297
|
+
* loads and allows analytics, as Hydrogen does. Set it when your storefront
|
|
298
|
+
* loads that API; the Hydrogen entries set it for you. Without it, a page
|
|
299
|
+
* where the API is absent follows `setConsent` or the server's view.
|
|
300
|
+
*/
|
|
301
|
+
expectCustomerPrivacy?: boolean;
|
|
302
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abconvert/headless",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "ABConvert A/B tests on a custom Shopify storefront: assignment, test prices, and cart attributes on the server, edge, or browser.",
|
|
5
|
+
"keywords": ["abconvert", "shopify", "headless", "hydrogen", "nextjs", "ab-testing", "storefront-api"],
|
|
6
|
+
"homepage": "https://docs.abconvert.io/headless-sdk/overview",
|
|
7
|
+
"bugs": { "email": "support@abconvert.io" },
|
|
8
|
+
"repository": { "type": "git", "url": "git+https://github.com/ABConvert/ab-test-app.git", "directory": "packages/headless" },
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.cjs",
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
17
|
+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
|
|
18
|
+
},
|
|
19
|
+
"./react": {
|
|
20
|
+
"import": { "types": "./dist/react.d.ts", "default": "./dist/react.js" },
|
|
21
|
+
"require": { "types": "./dist/react.d.cts", "default": "./dist/react.cjs" }
|
|
22
|
+
},
|
|
23
|
+
"./next": {
|
|
24
|
+
"import": { "types": "./dist/next.d.ts", "default": "./dist/next.js" },
|
|
25
|
+
"require": { "types": "./dist/next.d.cts", "default": "./dist/next.cjs" }
|
|
26
|
+
},
|
|
27
|
+
"./hydrogen": {
|
|
28
|
+
"import": { "types": "./dist/hydrogen.d.ts", "default": "./dist/hydrogen.js" },
|
|
29
|
+
"require": { "types": "./dist/hydrogen.d.cts", "default": "./dist/hydrogen.cjs" }
|
|
30
|
+
},
|
|
31
|
+
"./hydrogen/analytics-provider": {
|
|
32
|
+
"import": { "types": "./dist/hydrogen-analytics-provider.d.ts", "default": "./dist/hydrogen-analytics-provider.js" },
|
|
33
|
+
"require": { "types": "./dist/hydrogen-analytics-provider.d.cts", "default": "./dist/hydrogen-analytics-provider.cjs" }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"typesVersions": {
|
|
37
|
+
"*": {
|
|
38
|
+
"react": ["./dist/react.d.ts"],
|
|
39
|
+
"next": ["./dist/next.d.ts"],
|
|
40
|
+
"hydrogen": ["./dist/hydrogen.d.ts"],
|
|
41
|
+
"hydrogen/analytics-provider": ["./dist/hydrogen-analytics-provider.d.ts"]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": ["dist", "!dist/*inputs.json", "NOTICE"],
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"engines": { "node": ">=20" },
|
|
47
|
+
"publishConfig": { "access": "public" },
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "node scripts/build.mjs",
|
|
50
|
+
"test": "vitest run --config vitest.config.ts",
|
|
51
|
+
"test:watch": "vitest --config vitest.config.ts",
|
|
52
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
53
|
+
"check:runtime": "node scripts/check-runtime.mjs",
|
|
54
|
+
"check": "npm run typecheck && npm test && npm run build && npm run check:runtime"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"react": ">=17",
|
|
58
|
+
"next": ">=14",
|
|
59
|
+
"@shopify/hydrogen": ">=2024.4"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"react": { "optional": true },
|
|
63
|
+
"next": { "optional": true },
|
|
64
|
+
"@shopify/hydrogen": { "optional": true }
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@types/react": "18.3.28",
|
|
68
|
+
"@types/react-dom": "18.3.7",
|
|
69
|
+
"esbuild": "0.27.4",
|
|
70
|
+
"jsdom": "25.0.1",
|
|
71
|
+
"react": "18.3.1",
|
|
72
|
+
"react-dom": "18.3.1",
|
|
73
|
+
"typescript": "4.9.5",
|
|
74
|
+
"vitest": "1.6.1"
|
|
75
|
+
}
|
|
76
|
+
}
|