@avsbhq/snippet-types 1.0.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/LICENSE +21 -0
- package/README.md +129 -0
- package/avsb.d.ts +413 -0
- package/package.json +45 -0
- package/variation-code.d.ts +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AvsB Platform
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# @avsbhq/snippet-types
|
|
2
|
+
|
|
3
|
+
TypeScript types for `window.avsb`, the A vs B web-snippet API.
|
|
4
|
+
|
|
5
|
+
Types only. Nothing is bundled, nothing runs, and your production build is
|
|
6
|
+
unchanged: the snippet itself arrives from the A vs B CDN through the install tag
|
|
7
|
+
you paste into your site.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install --save-dev @avsbhq/snippet-types
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Use it
|
|
16
|
+
|
|
17
|
+
These are global declarations, so add the package to `types` in your
|
|
18
|
+
`tsconfig.json` rather than importing it:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"compilerOptions": {
|
|
23
|
+
"types": ["@avsbhq/snippet-types"]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you would rather not touch `types` (which replaces the default "include every
|
|
29
|
+
installed type package" behaviour), reference it from any `.d.ts` in your project
|
|
30
|
+
instead:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// docs-example: not typechecked here, because the A vs B repository does not install @avsbhq/snippet-types as a dependency, so the type reference has nothing to resolve against
|
|
34
|
+
|
|
35
|
+
/// <reference types="@avsbhq/snippet-types" />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Either way, `avsb` and `window.avsb` are typed everywhere, and every shape has a
|
|
39
|
+
name you can use:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
avsb.ready?.(() => {
|
|
43
|
+
avsb.track.event('signup_completed', { revenue: 49.99 });
|
|
44
|
+
|
|
45
|
+
const variation: string | null = avsb.getVariation('exp_id_from_the_dashboard');
|
|
46
|
+
if (variation) console.log('running', variation, 'on snippet', avsb.version);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function reportOrder(order: AvsbPurchaseOrder): void {
|
|
50
|
+
avsb.track.purchase(order);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const unsubscribe = avsb.on?.('event', (record: AvsbIntegrationRecord) => {
|
|
54
|
+
if (record.event_type === 'exposure') myAnalytics.track('AB exposure', { ...record });
|
|
55
|
+
});
|
|
56
|
+
unsubscribe?.();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`ready`, `on` and `consent` are optional on `AvsbApi` because the preview bypass
|
|
60
|
+
runtime does not define them, so reach them with `?.` as above.
|
|
61
|
+
|
|
62
|
+
`getVisitorId()` is typed `string | null`, because there is no visitor id before
|
|
63
|
+
`avsb.init()` in consent mode, and none at all for a visitor who denied
|
|
64
|
+
analytics:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const visitorId: string | null = avsb.getVisitorId();
|
|
68
|
+
if (visitorId) sendToYourBackend(visitorId);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## The names
|
|
72
|
+
|
|
73
|
+
`AvsbApi` is the type of `window.avsb`. The shapes it uses are all named too:
|
|
74
|
+
`AvsbTrackFunction`, `AvsbTrackEventProperties`, `AvsbPurchaseOrder`,
|
|
75
|
+
`AvsbPurchaseItem`, `AvsbIntegrationRecord`, `AvsbIntegrationEventType`,
|
|
76
|
+
`AvsbConsentApi`, `AvsbConsentState`, `AvsbDatasetHandle`,
|
|
77
|
+
`AvsbDatasetGetResult`, `AvsbRecsClient`, `AvsbRecsRequest`, `AvsbRecsResult`,
|
|
78
|
+
`AvsbRecProduct`, `AvsbCommerceApi`, `AvsbContractProduct`, `AvsbContractCart`,
|
|
79
|
+
`AvsbContractCartLine`, `AvsbUtils`, `AvsbWaitUntilFn`, `AvsbWaitTarget`,
|
|
80
|
+
`AvsbWaitOptions`, `AvsbGuardedThenable`, `AvsbCookieApi`, `AvsbKVStore`,
|
|
81
|
+
`AvsbElementProps`.
|
|
82
|
+
|
|
83
|
+
Every one of them is global, so no import is needed, and every one is stable:
|
|
84
|
+
names here are only added, never renamed or removed.
|
|
85
|
+
|
|
86
|
+
## Writing variation, trigger, or project code
|
|
87
|
+
|
|
88
|
+
Code written inside the A vs B editor gets an `options` toolkit and the
|
|
89
|
+
`initVariation` / `initTrigger` / `initProject` entry points. Those are authoring
|
|
90
|
+
globals, so they live behind a subpath and stay out of your site's global scope:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"compilerOptions": {
|
|
95
|
+
"types": ["@avsbhq/snippet-types", "@avsbhq/snippet-types/variation-code"]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
function initVariation(options: AvsbVariationOptions): void {
|
|
102
|
+
options.waitUntil<HTMLElement>('.hero h1').then((el) => {
|
|
103
|
+
options.utils.setText(el, 'A clearer headline');
|
|
104
|
+
options.track.event('hero_seen');
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The A vs B CLI (`avsb clone`) already writes this configuration for you when it
|
|
110
|
+
scaffolds a local experiment project.
|
|
111
|
+
|
|
112
|
+
## Calling the API before the snippet has loaded
|
|
113
|
+
|
|
114
|
+
The loader tag is `async`, so the API can arrive after your own code runs. The
|
|
115
|
+
install tag defines `avsb.ready`, `avsb.on`, `avsb.consent.set` and the four
|
|
116
|
+
`avsb.track.*` methods from the first byte of the page, so those calls are queued
|
|
117
|
+
and replayed. Everything else belongs inside `avsb.ready(...)`.
|
|
118
|
+
|
|
119
|
+
## Versioning
|
|
120
|
+
|
|
121
|
+
Published in lockstep with the rest of the `@avsbhq/*` family. The declarations
|
|
122
|
+
mirror the snippet runtime's own types, and a test in the A vs B repository fails
|
|
123
|
+
if the two drift apart.
|
|
124
|
+
|
|
125
|
+
## Links
|
|
126
|
+
|
|
127
|
+
- [Snippet SDK API reference](https://avsb.cloud/docs/developer-reference/snippet-sdk-api)
|
|
128
|
+
- [Tracking events](https://avsb.cloud/docs/developer-reference/tracking-events)
|
|
129
|
+
- [Debug mode](https://avsb.cloud/docs/troubleshooting/debug-mode)
|
package/avsb.d.ts
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// TypeScript declarations for `window.avsb`, the A vs B web-snippet API.
|
|
2
|
+
//
|
|
3
|
+
// SOURCE OF TRUTH for two consumers: this package (`@avsbhq/snippet-types`), and
|
|
4
|
+
// the dashboard code editor's ambient lib at `snippet/types/avsb.d.ts`, which is
|
|
5
|
+
// a generated copy (run `npm run sync:types` here after editing; `sync.test.ts`
|
|
6
|
+
// fails if it drifts).
|
|
7
|
+
//
|
|
8
|
+
// Every shape mirrors the runtime's own types in `snippet/src/types.ts` (plus
|
|
9
|
+
// `utils/types.ts`, `purchase.ts`, `datasets.ts`, `recs.ts`, `commerceTypes.ts`),
|
|
10
|
+
// and `sync.test.ts` asserts that `AvsbApi` and `AvsbTrackFunction` carry exactly
|
|
11
|
+
// the members of `AvsbPublicAPI` and `TrackFunction` there, so an API added to the
|
|
12
|
+
// snippet cannot quietly go missing here.
|
|
13
|
+
//
|
|
14
|
+
// Two rules for editing:
|
|
15
|
+
//
|
|
16
|
+
// - Keep every interface member on ONE line. The sync test reads members by line.
|
|
17
|
+
// - Never add an `import` or `export`. That turns the file into a module, which
|
|
18
|
+
// hides every name below and stops `interface Window` merging with the
|
|
19
|
+
// browser's own, so consumers following the documented setup get nothing.
|
|
20
|
+
// Names are FROZEN at launch: rename nothing, add only.
|
|
21
|
+
|
|
22
|
+
// --- Tracking ---
|
|
23
|
+
|
|
24
|
+
interface AvsbTrackEventProperties {
|
|
25
|
+
/** Monetary amount for this event. Feeds revenue metrics. Plain number, no currency symbol. */
|
|
26
|
+
revenue?: number
|
|
27
|
+
/** Continuous measurement (order value, load time, scroll depth) for percentile and value metrics. */
|
|
28
|
+
value?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface AvsbPurchaseItem {
|
|
32
|
+
sku: string
|
|
33
|
+
/** Parent product id when `sku` is a variant. Defaults to `sku` server-side. */
|
|
34
|
+
productKey?: string
|
|
35
|
+
name?: string
|
|
36
|
+
price?: number
|
|
37
|
+
quantity?: number
|
|
38
|
+
category?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** An order passed to `avsb.track.purchase`. Money is decimal, in the project currency. */
|
|
42
|
+
interface AvsbPurchaseOrder {
|
|
43
|
+
/** Unique order id. Required. */
|
|
44
|
+
orderId: string
|
|
45
|
+
/** Order total as a decimal, e.g. 99.95. Required. */
|
|
46
|
+
total: number
|
|
47
|
+
/** ISO 4217 code. Defaults to the project currency. */
|
|
48
|
+
currency?: string
|
|
49
|
+
subtotal?: number
|
|
50
|
+
shipping?: number
|
|
51
|
+
tax?: number
|
|
52
|
+
discount?: number
|
|
53
|
+
coupon?: string
|
|
54
|
+
/** Stored but excluded from every calculation. For testing your integration. */
|
|
55
|
+
test?: boolean
|
|
56
|
+
items?: AvsbPurchaseItem[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface AvsbTrackFunction {
|
|
60
|
+
/** Fire a custom event by its metric event key. Numbers are coerced to strings, so a numeric key works when the metric's key is those digits. An unmatched key warns and lists the keys that do work. */
|
|
61
|
+
event(eventKey: string | number, properties?: AvsbTrackEventProperties): void
|
|
62
|
+
/** Record a visitor attribute you can slice results by. */
|
|
63
|
+
segment(segmentKey: string, segmentValue: string): void
|
|
64
|
+
/** Send an order immediately. Not batched. */
|
|
65
|
+
purchase(order: AvsbPurchaseOrder): void
|
|
66
|
+
/** Tell the snippet the current cart total, for commerce audience conditions. `totalMinor` wins when both are set. */
|
|
67
|
+
cart(cart: { total?: number; totalMinor?: number }): void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- Event bus record ---
|
|
71
|
+
|
|
72
|
+
type AvsbIntegrationEventType =
|
|
73
|
+
| 'exposure'
|
|
74
|
+
| 'goal'
|
|
75
|
+
| 'purchase'
|
|
76
|
+
| 'rec_impression'
|
|
77
|
+
| 'rec_click'
|
|
78
|
+
| 'custom'
|
|
79
|
+
| 'test'
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The record delivered to `avsb.on('event')` subscribers, one per dispatched
|
|
83
|
+
* exposure, goal, purchase, or rec event. snake_case, shaped for analytics tools.
|
|
84
|
+
*/
|
|
85
|
+
interface AvsbIntegrationRecord {
|
|
86
|
+
/** Experiment shortId as a string. */
|
|
87
|
+
experiment_id: string
|
|
88
|
+
/** Display name; falls back to the shortId, never an empty string. */
|
|
89
|
+
experiment_name: string
|
|
90
|
+
/** Variation shortId as a string. */
|
|
91
|
+
variation_id: string
|
|
92
|
+
/** Display name; falls back to the shortId, never an empty string. */
|
|
93
|
+
variation_name: string
|
|
94
|
+
event_type: AvsbIntegrationEventType
|
|
95
|
+
/** Rendered event name (native defaults plus your configured overrides). */
|
|
96
|
+
event_name: string
|
|
97
|
+
/** UUID; matches the AvsB event id for tracker-queued events. */
|
|
98
|
+
event_id: string
|
|
99
|
+
revenue: number | null
|
|
100
|
+
value: number | null
|
|
101
|
+
/** Purchases only. */
|
|
102
|
+
currency?: string
|
|
103
|
+
/** Purchases only: the order id. */
|
|
104
|
+
transaction_id?: string
|
|
105
|
+
/** Goals and custom events: metric `type:shortId` or the developer eventKey. */
|
|
106
|
+
goal_key?: string
|
|
107
|
+
visitor_id: string
|
|
108
|
+
page_url: string
|
|
109
|
+
timestamp: number
|
|
110
|
+
sdk_version: string
|
|
111
|
+
/** Recommendation product data (recipe, items, position, ...). */
|
|
112
|
+
attributes?: Record<string, string | number>
|
|
113
|
+
/** Set to 'preview' or 'internal' on QA traffic; absent for live visitors. */
|
|
114
|
+
source?: 'preview' | 'internal'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Waiting ---
|
|
118
|
+
|
|
119
|
+
// A wait target: a CSS selector, a `window.x.y` global path, or a predicate.
|
|
120
|
+
type AvsbWaitTarget<T = unknown> = string | (() => T)
|
|
121
|
+
|
|
122
|
+
interface AvsbWaitOptions {
|
|
123
|
+
/** Milliseconds before the wait rejects. Default 10000. */
|
|
124
|
+
timeout?: number
|
|
125
|
+
/** Resolve a selector with an array of all matches (querySelectorAll). */
|
|
126
|
+
all?: boolean
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Promise-like whose `.catch()` is OPTIONAL: a failed wait logs gracefully rather
|
|
130
|
+
// than raising an unhandled rejection. `.stop()` cancels it.
|
|
131
|
+
interface AvsbGuardedThenable<T> extends PromiseLike<T> {
|
|
132
|
+
then<R1 = T, R2 = never>(
|
|
133
|
+
onFulfilled?: ((value: T) => R1 | PromiseLike<R1>) | null,
|
|
134
|
+
onRejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null
|
|
135
|
+
): AvsbGuardedThenable<R1 | R2>
|
|
136
|
+
catch<R = never>(
|
|
137
|
+
onRejected?: ((reason: unknown) => R | PromiseLike<R>) | null
|
|
138
|
+
): AvsbGuardedThenable<T | R>
|
|
139
|
+
finally(onFinally?: (() => void) | null): AvsbGuardedThenable<T>
|
|
140
|
+
stop(): void
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface AvsbWaitUntilFn {
|
|
144
|
+
/** Wait for a selector / `window.x.y` path / predicate to resolve. */
|
|
145
|
+
<T = unknown>(target: AvsbWaitTarget<T>, opts?: AvsbWaitOptions): AvsbGuardedThenable<T>
|
|
146
|
+
/** Wait for several targets at once; resolves with an array of results. */
|
|
147
|
+
<T extends unknown[]>(targets: AvsbWaitTarget[], opts?: AvsbWaitOptions): AvsbGuardedThenable<T>
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- Consent ---
|
|
151
|
+
|
|
152
|
+
/** Per-category consent flags. An absent key means "not expressed" (allowed). */
|
|
153
|
+
interface AvsbConsentState {
|
|
154
|
+
analytics?: boolean
|
|
155
|
+
marketing?: boolean
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Visitor consent, the hook your cookie banner calls. `set` persists for 1 year in
|
|
160
|
+
* the first-party `avsb_consent` cookie and wins over the inferred Google signal.
|
|
161
|
+
*/
|
|
162
|
+
interface AvsbConsentApi {
|
|
163
|
+
set(state: AvsbConsentState): void
|
|
164
|
+
get(): AvsbConsentState
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- Datasets ---
|
|
168
|
+
|
|
169
|
+
interface AvsbDatasetGetResult {
|
|
170
|
+
found: boolean
|
|
171
|
+
/** FEED only: the row's ordered items array. */
|
|
172
|
+
items?: unknown[]
|
|
173
|
+
/** TABLE only: the full row object. */
|
|
174
|
+
row?: unknown
|
|
175
|
+
/** LIST only: whether the key is a member of the list. */
|
|
176
|
+
member?: boolean
|
|
177
|
+
meta: { slug: string; version: number | null }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface AvsbDatasetHandle {
|
|
181
|
+
get(key: string): Promise<AvsbDatasetGetResult>
|
|
182
|
+
has(key: string): Promise<boolean>
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Recommendations ---
|
|
186
|
+
|
|
187
|
+
interface AvsbRecProduct {
|
|
188
|
+
id: string
|
|
189
|
+
title?: string
|
|
190
|
+
image?: string
|
|
191
|
+
href?: string
|
|
192
|
+
category?: string
|
|
193
|
+
/** MINOR units (ISO-4217). */
|
|
194
|
+
price?: number
|
|
195
|
+
/** MINOR units. */
|
|
196
|
+
compareAtPrice?: number
|
|
197
|
+
currency?: string
|
|
198
|
+
availability?: 'in_stock' | 'out_of_stock' | 'removed'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface AvsbRecsRequest {
|
|
202
|
+
recipe: string
|
|
203
|
+
context?: { seed?: string; seeds?: string[]; recentlyViewed?: number }
|
|
204
|
+
maxItems?: number
|
|
205
|
+
excludeOutOfStock?: boolean
|
|
206
|
+
/** Which rec surface this call serves, for multi-surface pages. */
|
|
207
|
+
surface?: string
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface AvsbRecsResult {
|
|
211
|
+
items: AvsbRecProduct[]
|
|
212
|
+
served: boolean
|
|
213
|
+
recipe: string
|
|
214
|
+
/** Slug of the chain step that served, null on a miss. */
|
|
215
|
+
source: string | null
|
|
216
|
+
fallbackStep: number | null
|
|
217
|
+
/** True when the resolve failed (timeout, network, HTTP, parse). */
|
|
218
|
+
degraded: boolean
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface AvsbRecsClient {
|
|
222
|
+
/** Resolve items for a recipe. Auto-fires rec:impression on success. Never throws. */
|
|
223
|
+
get(req: AvsbRecsRequest): Promise<AvsbRecsResult>
|
|
224
|
+
/** Fire a rec:click for a product the visitor clicked. */
|
|
225
|
+
trackClick(productId: string, opts?: { position?: number }): void
|
|
226
|
+
/** Record a product view (feeds recently-viewed and commerce audiences). */
|
|
227
|
+
trackView(sku: string, meta?: { category?: string }): void
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- Commerce data contract ---
|
|
231
|
+
|
|
232
|
+
interface AvsbContractProduct {
|
|
233
|
+
/** Required. The merchant's product id. */
|
|
234
|
+
sku: string
|
|
235
|
+
/** Product-grain canonical key. Defaults to `sku`. */
|
|
236
|
+
productKey?: string
|
|
237
|
+
title?: string
|
|
238
|
+
/** Major units as displayed. */
|
|
239
|
+
price?: number
|
|
240
|
+
/** Already-minor integer. Wins over `price`. */
|
|
241
|
+
priceMinor?: number
|
|
242
|
+
compareAtPrice?: number
|
|
243
|
+
compareAtPriceMinor?: number
|
|
244
|
+
currency?: string
|
|
245
|
+
image?: string
|
|
246
|
+
url?: string
|
|
247
|
+
brand?: string
|
|
248
|
+
category?: string
|
|
249
|
+
categories?: string[]
|
|
250
|
+
availability?: 'in_stock' | 'out_of_stock' | 'preorder'
|
|
251
|
+
stock?: number
|
|
252
|
+
variant?: { variantSku: string; options?: Record<string, string>; price?: number; priceMinor?: number; stock?: number }
|
|
253
|
+
customFields?: Record<string, string | number | boolean>
|
|
254
|
+
/** ms-epoch product createdAt. */
|
|
255
|
+
createdAt?: number
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
interface AvsbContractCartLine {
|
|
259
|
+
sku: string
|
|
260
|
+
productKey?: string
|
|
261
|
+
quantity: number
|
|
262
|
+
price?: number
|
|
263
|
+
priceMinor?: number
|
|
264
|
+
variantSku?: string
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface AvsbContractCart {
|
|
268
|
+
items: AvsbContractCartLine[]
|
|
269
|
+
total?: number
|
|
270
|
+
totalMinor?: number
|
|
271
|
+
currency?: string
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
interface AvsbCommerceApi {
|
|
275
|
+
productView(p: AvsbContractProduct): void
|
|
276
|
+
/** Category / collection impressions. */
|
|
277
|
+
productList(items: AvsbContractProduct[], meta?: { listId?: string }): void
|
|
278
|
+
addToCart(line: AvsbContractCartLine): void
|
|
279
|
+
removeFromCart(line: AvsbContractCartLine): void
|
|
280
|
+
/** Full cart snapshot. */
|
|
281
|
+
cart(cart: AvsbContractCart): void
|
|
282
|
+
checkoutStep(step: { step: number | string; cart?: AvsbContractCart }): void
|
|
283
|
+
search(q: { query: string; resultCount?: number; skus?: string[] }): void
|
|
284
|
+
categoryView(c: { category: string; skus?: string[] }): void
|
|
285
|
+
wishlist(w: { sku: string; action: 'add' | 'remove' }): void
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// --- Utils toolkit (avsb.utils.* and options.utils.*) ---
|
|
289
|
+
interface AvsbCookieApi {
|
|
290
|
+
get(name: string): string | null
|
|
291
|
+
set(name: string, value: string, opts?: { days?: number; path?: string; domain?: string; sameSite?: 'Lax' | 'Strict' | 'None'; secure?: boolean }): void
|
|
292
|
+
remove(name: string): void
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
interface AvsbKVStore {
|
|
296
|
+
get<T = unknown>(key: string): T | null
|
|
297
|
+
set(key: string, value: unknown): void
|
|
298
|
+
remove(key: string): void
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
interface AvsbElementProps {
|
|
302
|
+
class?: string
|
|
303
|
+
id?: string
|
|
304
|
+
text?: string
|
|
305
|
+
html?: string
|
|
306
|
+
attrs?: Record<string, string>
|
|
307
|
+
style?: Record<string, string>
|
|
308
|
+
on?: Record<string, EventListener>
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
interface AvsbUtils {
|
|
312
|
+
/** Guarded, promise-based wait. `.catch()` optional; failures log gracefully. */
|
|
313
|
+
waitUntil: AvsbWaitUntilFn
|
|
314
|
+
/** Alias of waitUntil (Qubit `poll` compatibility). */
|
|
315
|
+
poll: AvsbWaitUntilFn
|
|
316
|
+
/** Resolve once a single selector is present. */
|
|
317
|
+
waitForElement(selector: string, opts?: { timeout?: number; root?: ParentNode }): AvsbGuardedThenable<Element>
|
|
318
|
+
/** Run a callback for each matching element, now and on future mutations. Supports shadow roots via `root`. */
|
|
319
|
+
onMutation(selector: string, callback: (el: Element) => void, opts?: { root?: ParentNode; once?: boolean; attributes?: boolean }): { stop(): void }
|
|
320
|
+
once<F extends (...args: never[]) => unknown>(fn: F): F
|
|
321
|
+
throttle<F extends (...args: never[]) => void>(fn: F, ms: number): F & { cancel(): void }
|
|
322
|
+
debounce<F extends (...args: never[]) => void>(fn: F, ms: number): F & { cancel(): void }
|
|
323
|
+
raf(fn: FrameRequestCallback): { cancel(): void }
|
|
324
|
+
idle(fn: () => void, opts?: { timeout?: number }): { cancel(): void }
|
|
325
|
+
domReady(): AvsbGuardedThenable<void>
|
|
326
|
+
retry<T>(fn: () => Promise<T> | T, opts?: { attempts?: number; delay?: number; factor?: number }): Promise<T>
|
|
327
|
+
$(selector: string, root?: ParentNode): Element | null
|
|
328
|
+
$$(selector: string, root?: ParentNode): Element[]
|
|
329
|
+
createElement(tag: string, props?: AvsbElementProps, children?: (Node | string)[]): HTMLElement
|
|
330
|
+
insertAfter(newNode: Node, ref: Node): void
|
|
331
|
+
insertBefore(newNode: Node, ref: Node): void
|
|
332
|
+
wrap(node: Node, wrapper: HTMLElement): void
|
|
333
|
+
remove(node: Node): void
|
|
334
|
+
setText(node: Node, text: string): void
|
|
335
|
+
/** Sets innerHTML; scrubs scripts and handlers unless `{ raw: true }`. */
|
|
336
|
+
setHtml(node: Element, html: string, opts?: { raw?: boolean }): void
|
|
337
|
+
/** Direct listener, or (with a selector string target) a delegated listener that also catches future nodes. */
|
|
338
|
+
on(target: EventTarget | string, type: string, handler: (event: Event, matched?: Element) => void, opts?: AddEventListenerOptions): { off(): void }
|
|
339
|
+
/** Fires on client-side route changes. */
|
|
340
|
+
onUrlChange(callback: (url: string) => void): { off(): void }
|
|
341
|
+
cookie: AvsbCookieApi
|
|
342
|
+
query: { get(name: string): string | null; getAll(): Record<string, string> }
|
|
343
|
+
storage: { local: AvsbKVStore; session: AvsbKVStore }
|
|
344
|
+
/** Share values between triggers.js and variation.js of the same experiment. */
|
|
345
|
+
state: { get<T = unknown>(key: string): T | undefined; set(key: string, value: unknown): void }
|
|
346
|
+
/** Scoped logger: debug/info emit only in debug, preview, or dev mode; warn always. */
|
|
347
|
+
log: { debug(...args: unknown[]): void; info(...args: unknown[]): void; warn(...args: unknown[]): void }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// --- Experiment decisions ---
|
|
351
|
+
|
|
352
|
+
/** Why an experiment did or did not run for this visit. `variant` / `control` mean the visitor is IN the test. */
|
|
353
|
+
type AvsbDecisionStatus = 'variant' | 'control' | 'trigger-pending' | 'not-targeted' | 'audience-mismatch' | 'excluded' | 'traffic-holdout' | 'capped' | 'scheduled-off' | 'error'
|
|
354
|
+
|
|
355
|
+
/** One row of `avsb.getExperimentDecisions()`. The variation fields are present only when a variation was assigned. */
|
|
356
|
+
interface AvsbExperimentDecision {
|
|
357
|
+
experimentId: string
|
|
358
|
+
status: AvsbDecisionStatus
|
|
359
|
+
variationId?: string
|
|
360
|
+
variationName?: string
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// --- The global API ---
|
|
364
|
+
|
|
365
|
+
interface AvsbApi {
|
|
366
|
+
/** Consent mode only: start the snippet once the visitor has agreed. No effect otherwise. */
|
|
367
|
+
init(): void | Promise<void>
|
|
368
|
+
/** Stop everything and forget the visitor: tracking off, timers and listeners cleared, variations reverted, visitor cookie expired. */
|
|
369
|
+
disable(): void
|
|
370
|
+
track: AvsbTrackFunction
|
|
371
|
+
/** The visitor's id, or null when there isn't one (before `init()` in consent mode, or for a visitor who denied analytics). */
|
|
372
|
+
getVisitorId(): string | null
|
|
373
|
+
/** Replace the visitor id to stitch a browser onto a logged-in account. Drops sticky assignments and re-evaluates, so the variation can change. Returns false for an unusable id. */
|
|
374
|
+
setVisitorId(visitorId: string): boolean
|
|
375
|
+
/** The variation this visitor is assigned in the experiment, or null. Pass the numeric **Experiment ID** from the experiment details panel (a number or its string form); the value returned is that variation's numeric **Variation ID**. Internal UUIDs are not accepted. */
|
|
376
|
+
getVariation(experimentId: string | number): string | null
|
|
377
|
+
/** Force a variation, for QA, by the numeric **Experiment ID** and **Variation ID** shown in the experiment details panel. `{ track: false }` applies it without recording an exposure. */
|
|
378
|
+
forceVariation(experimentId: string | number, variationId: string | number, options?: { track?: boolean }): boolean
|
|
379
|
+
waitUntil: AvsbWaitUntilFn
|
|
380
|
+
utils: AvsbUtils
|
|
381
|
+
/** Subscribe to every experiment event. Returns an unsubscribe function. Absent in the preview bypass runtime. */
|
|
382
|
+
on?(event: 'event', callback: (record: AvsbIntegrationRecord) => void): () => void
|
|
383
|
+
/** Run a callback once experiments have been evaluated. Safe to call before the snippet loads. */
|
|
384
|
+
ready?(callback: () => void): void
|
|
385
|
+
/** @deprecated Use `ready`. Same machinery. */
|
|
386
|
+
onReady(callback: () => void): void
|
|
387
|
+
/** Visitor consent: `set` from your banner, `get` for the effective state. Absent in the preview bypass runtime. */
|
|
388
|
+
consent?: AvsbConsentApi
|
|
389
|
+
/** Experiments this visitor is active in on this page, as the same numeric dashboard ids `getVariation` takes, in string form. */
|
|
390
|
+
getActiveExperiments(): Array<{ experimentId: string; variationId: string }>
|
|
391
|
+
/** One row per experiment on the page: what happened to this visit and why (held out, mismatched, pending, running). Rebuilt on every evaluation; empty before evaluation and after `disable()`. Optional: absent on snippet builds that predate it, so read it as `avsb.getExperimentDecisions?.()`. */
|
|
392
|
+
getExperimentDecisions?(): AvsbExperimentDecision[]
|
|
393
|
+
/** Read a server-side dataset. Never throws: unknown slugs resolve `{ found: false }`. */
|
|
394
|
+
dataset(slug: string): AvsbDatasetHandle
|
|
395
|
+
/** Headless recommendations. Inside variation code prefer `options.recs`, which attributes automatically. */
|
|
396
|
+
recs: AvsbRecsClient
|
|
397
|
+
/** Commerce events. Buffered until the commerce chunk loads. */
|
|
398
|
+
commerce: AvsbCommerceApi
|
|
399
|
+
/** Leave a shared preview link: clears the preview token and reloads. */
|
|
400
|
+
exitPreview(): void
|
|
401
|
+
/** Re-run every experiment against the page as it is now. For app-driven navigation the History API never sees. */
|
|
402
|
+
refresh(): void
|
|
403
|
+
/** The snippet build running on this page, e.g. `1.1.0`. */
|
|
404
|
+
version: string
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** `window.avsb`. `q` is the install tag's early-call queue, drained by the runtime. */
|
|
408
|
+
interface Window {
|
|
409
|
+
avsb: AvsbApi & { q?: Array<[string, ...unknown[]]> }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** The same object as `window.avsb`, so `avsb.track.event(...)` reads cleanly. */
|
|
413
|
+
declare const avsb: AvsbApi
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@avsbhq/snippet-types",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeScript declarations for window.avsb, the A vs B web-snippet API",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"avsb",
|
|
7
|
+
"ab-testing",
|
|
8
|
+
"experiments",
|
|
9
|
+
"types",
|
|
10
|
+
"typescript",
|
|
11
|
+
"snippet"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"types": "./avsb.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./avsb.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./variation-code": {
|
|
21
|
+
"types": "./variation-code.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"avsb.d.ts",
|
|
27
|
+
"variation-code.d.ts"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/avsbhq/a-vs-b.git",
|
|
35
|
+
"directory": "packages/avsb-snippet-types"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/avsbhq/a-vs-b/tree/main/packages/avsb-snippet-types#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/avsbhq/a-vs-b/issues"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"sync:types": "node ../../snippet/scripts/syncSnippetTypes.mjs",
|
|
43
|
+
"check:types": "node ../../snippet/scripts/syncSnippetTypes.mjs --check"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// TypeScript declarations for code you write INSIDE A vs B: variation code,
|
|
2
|
+
// trigger code, and project code. Companion to `avsb.d.ts`, which types the
|
|
3
|
+
// `window.avsb` global and must be loaded alongside this file.
|
|
4
|
+
//
|
|
5
|
+
// Kept separate because these are authoring globals (`options`, `activate`,
|
|
6
|
+
// `initVariation`, ...). A site consuming `@avsbhq/snippet-types` for
|
|
7
|
+
// `window.avsb` should not have them in its global scope, so they live behind
|
|
8
|
+
// the `@avsbhq/snippet-types/variation-code` subpath.
|
|
9
|
+
//
|
|
10
|
+
// SOURCE OF TRUTH. The dashboard code editor's copy at
|
|
11
|
+
// `snippet/types/variation-code.d.ts` is generated: run `npm run sync:types` in
|
|
12
|
+
// this package after editing. Mirrors `HarnessOptions` in
|
|
13
|
+
// `snippet/src/userCodeHarness.ts`.
|
|
14
|
+
//
|
|
15
|
+
// Not a module, for the same reason as `avsb.d.ts`: these are global names, and
|
|
16
|
+
// an `export` would hide all of them. Frozen at launch: rename nothing, add only.
|
|
17
|
+
|
|
18
|
+
/** Recommendations client pre-bound to the running experiment and variation. */
|
|
19
|
+
interface AvsbBoundRecsClient extends AvsbRecsClient {
|
|
20
|
+
/** Resolve THIS variation's bound recipe, so one code block serves every variation. */
|
|
21
|
+
getBound(req?: Omit<AvsbRecsRequest, 'recipe'>): Promise<AvsbRecsResult>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What every user-code surface receives. Self-cleaning helpers are torn down for you. */
|
|
25
|
+
interface AvsbCodeOptions {
|
|
26
|
+
/** Present in variation and trigger code. */
|
|
27
|
+
experimentId?: string
|
|
28
|
+
experimentName?: string
|
|
29
|
+
variationId?: string
|
|
30
|
+
variationName?: string
|
|
31
|
+
variationType?: 'control' | 'variant'
|
|
32
|
+
/** Present in project code only. */
|
|
33
|
+
projectId?: string
|
|
34
|
+
/** Runs when the variation is activated. In variation code it fires as soon as your function returns. */
|
|
35
|
+
onActivation(cb: () => void): void
|
|
36
|
+
/** Runs when the variation is removed (client-side navigation, teardown, forced switch). */
|
|
37
|
+
onRemove(cb: () => void): void
|
|
38
|
+
track: AvsbTrackFunction
|
|
39
|
+
/** Impressions and clicks attribute to this variation automatically. */
|
|
40
|
+
recs: AvsbBoundRecsClient
|
|
41
|
+
getVisitorId(): string | null
|
|
42
|
+
/** The visitor's numeric **Variation ID** in that experiment, or null. Takes the numeric **Experiment ID** from the experiment details panel, NOT the `experimentId` above (that one is the internal id). */
|
|
43
|
+
getVariation(experimentId: string | number): string | null
|
|
44
|
+
/** Experiments running on this page, as the same numeric dashboard ids. */
|
|
45
|
+
getActiveExperiments(): Array<{ experimentId: string; variationId: string }>
|
|
46
|
+
/** Force a variation by the numeric **Experiment ID** and **Variation ID** from the experiment details panel. */
|
|
47
|
+
forceVariation(experimentId: string | number, variationId: string | number, options?: { track?: boolean }): boolean
|
|
48
|
+
/** Cancelled automatically on teardown, unlike `avsb.waitUntil`. */
|
|
49
|
+
waitUntil: AvsbWaitUntilFn
|
|
50
|
+
utils: AvsbUtils
|
|
51
|
+
/** Same numeric handle as the browser's own. Cancelled for you on teardown. */
|
|
52
|
+
setTimeout(handler: () => void, ms?: number): number
|
|
53
|
+
setInterval(handler: () => void, ms?: number): number
|
|
54
|
+
addEventListener(target: EventTarget, type: string, handler: EventListenerOrEventListenerObject, opts?: AddEventListenerOptions | boolean): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type AvsbTriggerOptions = AvsbCodeOptions
|
|
58
|
+
type AvsbVariationOptions = AvsbCodeOptions
|
|
59
|
+
type AvsbProjectOptions = AvsbCodeOptions
|
|
60
|
+
|
|
61
|
+
// Ambient declarations for standalone authoring files. They look unused to
|
|
62
|
+
// ESLint because nothing in this declaration file references them: they are the
|
|
63
|
+
// names your own trigger, variation, and project code calls into, so they must
|
|
64
|
+
// stay literal.
|
|
65
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
66
|
+
declare const options: AvsbCodeOptions
|
|
67
|
+
declare function activate(): void
|
|
68
|
+
declare function deactivate(): void
|
|
69
|
+
declare function initVariation(options: AvsbVariationOptions): void
|
|
70
|
+
declare function initTrigger(options: AvsbTriggerOptions, activate: () => void, deactivate: () => void): void
|
|
71
|
+
declare function initProject(options: AvsbProjectOptions): void
|
|
72
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|