@absmartly/sdk-plugins 1.0.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/README.md +1101 -0
- package/dist/absmartly-cookie.min.js +1 -0
- package/dist/absmartly-dom-changes-core.min.js +1 -0
- package/dist/absmartly-dom-changes-lite.min.js +1 -0
- package/dist/absmartly-overrides-full.min.js +1 -0
- package/dist/absmartly-overrides-lite.min.js +1 -0
- package/dist/absmartly-sdk-plugins.min.js +1 -0
- package/dist/absmartly-vitals.min.js +1 -0
- package/es/cookies/CookiePlugin.js +410 -0
- package/es/cookies/CookiePlugin.js.map +1 -0
- package/es/cookies/index.js +2 -0
- package/es/cookies/index.js.map +1 -0
- package/es/core/DOMChangesPluginLite.js +1124 -0
- package/es/core/DOMChangesPluginLite.js.map +1 -0
- package/es/core/DOMManipulatorLite.js +410 -0
- package/es/core/DOMManipulatorLite.js.map +1 -0
- package/es/core/ExposureTracker.js +501 -0
- package/es/core/ExposureTracker.js.map +1 -0
- package/es/core/HTMLInjector.js +333 -0
- package/es/core/HTMLInjector.js.map +1 -0
- package/es/core/PendingChangeManager.js +440 -0
- package/es/core/PendingChangeManager.js.map +1 -0
- package/es/core/StyleSheetManager.js +78 -0
- package/es/core/StyleSheetManager.js.map +1 -0
- package/es/entries/cookie-only.js +10 -0
- package/es/entries/cookie-only.js.map +1 -0
- package/es/entries/dom-only.js +13 -0
- package/es/entries/dom-only.js.map +1 -0
- package/es/entries/dom-with-overrides-full.js +19 -0
- package/es/entries/dom-with-overrides-full.js.map +1 -0
- package/es/entries/dom-with-overrides-lite.js +15 -0
- package/es/entries/dom-with-overrides-lite.js.map +1 -0
- package/es/entries/overrides-full-only.js +15 -0
- package/es/entries/overrides-full-only.js.map +1 -0
- package/es/entries/overrides-lite-only.js +12 -0
- package/es/entries/overrides-lite-only.js.map +1 -0
- package/es/entries/vitals-only.js +10 -0
- package/es/entries/vitals-only.js.map +1 -0
- package/es/index.js +17 -0
- package/es/index.js.map +1 -0
- package/es/overrides/BrowserCookieAdapter.js +76 -0
- package/es/overrides/BrowserCookieAdapter.js.map +1 -0
- package/es/overrides/OverridesPlugin.js +903 -0
- package/es/overrides/OverridesPlugin.js.map +1 -0
- package/es/overrides/OverridesPluginFull.js +32 -0
- package/es/overrides/OverridesPluginFull.js.map +1 -0
- package/es/overrides/OverridesPluginLite.js +352 -0
- package/es/overrides/OverridesPluginLite.js.map +1 -0
- package/es/overrides/index.js +4 -0
- package/es/overrides/index.js.map +1 -0
- package/es/overrides/types.js +2 -0
- package/es/overrides/types.js.map +1 -0
- package/es/parsers/VariantExtractor.js +570 -0
- package/es/parsers/VariantExtractor.js.map +1 -0
- package/es/types/index.js +2 -0
- package/es/types/index.js.map +1 -0
- package/es/utils/URLMatcher.js +114 -0
- package/es/utils/URLMatcher.js.map +1 -0
- package/es/utils/debug.js +184 -0
- package/es/utils/debug.js.map +1 -0
- package/es/vitals/WebVitalsPlugin.js +379 -0
- package/es/vitals/WebVitalsPlugin.js.map +1 -0
- package/es/vitals/index.js +2 -0
- package/es/vitals/index.js.map +1 -0
- package/lib/cookies/CookiePlugin.d.ts +55 -0
- package/lib/cookies/CookiePlugin.js +413 -0
- package/lib/cookies/CookiePlugin.js.map +1 -0
- package/lib/cookies/index.d.ts +2 -0
- package/lib/cookies/index.js +6 -0
- package/lib/cookies/index.js.map +1 -0
- package/lib/core/DOMChangesPluginLite.d.ts +89 -0
- package/lib/core/DOMChangesPluginLite.js +1127 -0
- package/lib/core/DOMChangesPluginLite.js.map +1 -0
- package/lib/core/DOMManipulatorLite.d.ts +21 -0
- package/lib/core/DOMManipulatorLite.js +413 -0
- package/lib/core/DOMManipulatorLite.js.map +1 -0
- package/lib/core/ExposureTracker.d.ts +81 -0
- package/lib/core/ExposureTracker.js +504 -0
- package/lib/core/ExposureTracker.js.map +1 -0
- package/lib/core/HTMLInjector.d.ts +19 -0
- package/lib/core/HTMLInjector.js +336 -0
- package/lib/core/HTMLInjector.js.map +1 -0
- package/lib/core/PendingChangeManager.d.ts +31 -0
- package/lib/core/PendingChangeManager.js +443 -0
- package/lib/core/PendingChangeManager.js.map +1 -0
- package/lib/core/StyleSheetManager.d.ts +16 -0
- package/lib/core/StyleSheetManager.js +81 -0
- package/lib/core/StyleSheetManager.js.map +1 -0
- package/lib/entries/cookie-only.d.ts +8 -0
- package/lib/entries/cookie-only.js +12 -0
- package/lib/entries/cookie-only.js.map +1 -0
- package/lib/entries/dom-only.d.ts +9 -0
- package/lib/entries/dom-only.js +29 -0
- package/lib/entries/dom-only.js.map +1 -0
- package/lib/entries/dom-with-overrides-full.d.ts +15 -0
- package/lib/entries/dom-with-overrides-full.js +39 -0
- package/lib/entries/dom-with-overrides-full.js.map +1 -0
- package/lib/entries/dom-with-overrides-lite.d.ts +11 -0
- package/lib/entries/dom-with-overrides-lite.js +32 -0
- package/lib/entries/dom-with-overrides-lite.js.map +1 -0
- package/lib/entries/overrides-full-only.d.ts +11 -0
- package/lib/entries/overrides-full-only.js +33 -0
- package/lib/entries/overrides-full-only.js.map +1 -0
- package/lib/entries/overrides-lite-only.d.ts +8 -0
- package/lib/entries/overrides-lite-only.js +28 -0
- package/lib/entries/overrides-lite-only.js.map +1 -0
- package/lib/entries/vitals-only.d.ts +8 -0
- package/lib/entries/vitals-only.js +12 -0
- package/lib/entries/vitals-only.js.map +1 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +34 -0
- package/lib/index.js.map +1 -0
- package/lib/overrides/BrowserCookieAdapter.d.ts +6 -0
- package/lib/overrides/BrowserCookieAdapter.js +79 -0
- package/lib/overrides/BrowserCookieAdapter.js.map +1 -0
- package/lib/overrides/OverridesPlugin.d.ts +33 -0
- package/lib/overrides/OverridesPlugin.js +906 -0
- package/lib/overrides/OverridesPlugin.js.map +1 -0
- package/lib/overrides/OverridesPluginFull.d.ts +9 -0
- package/lib/overrides/OverridesPluginFull.js +35 -0
- package/lib/overrides/OverridesPluginFull.js.map +1 -0
- package/lib/overrides/OverridesPluginLite.d.ts +27 -0
- package/lib/overrides/OverridesPluginLite.js +355 -0
- package/lib/overrides/OverridesPluginLite.js.map +1 -0
- package/lib/overrides/index.d.ts +3 -0
- package/lib/overrides/index.js +23 -0
- package/lib/overrides/index.js.map +1 -0
- package/lib/overrides/types.d.ts +43 -0
- package/lib/overrides/types.js +3 -0
- package/lib/overrides/types.js.map +1 -0
- package/lib/parsers/VariantExtractor.d.ts +49 -0
- package/lib/parsers/VariantExtractor.js +573 -0
- package/lib/parsers/VariantExtractor.js.map +1 -0
- package/lib/types/index.d.ts +177 -0
- package/lib/types/index.js +3 -0
- package/lib/types/index.js.map +1 -0
- package/lib/utils/URLMatcher.d.ts +20 -0
- package/lib/utils/URLMatcher.js +117 -0
- package/lib/utils/URLMatcher.js.map +1 -0
- package/lib/utils/debug.d.ts +41 -0
- package/lib/utils/debug.js +195 -0
- package/lib/utils/debug.js.map +1 -0
- package/lib/vitals/WebVitalsPlugin.d.ts +37 -0
- package/lib/vitals/WebVitalsPlugin.js +382 -0
- package/lib/vitals/WebVitalsPlugin.js.map +1 -0
- package/lib/vitals/index.d.ts +2 -0
- package/lib/vitals/index.js +6 -0
- package/lib/vitals/index.js.map +1 -0
- package/package.json +94 -0
- package/types/cookies/CookiePlugin.d.ts +55 -0
- package/types/cookies/index.d.ts +2 -0
- package/types/core/DOMChangesPluginLite.d.ts +89 -0
- package/types/core/DOMManipulatorLite.d.ts +21 -0
- package/types/core/ExposureTracker.d.ts +81 -0
- package/types/core/HTMLInjector.d.ts +19 -0
- package/types/core/PendingChangeManager.d.ts +31 -0
- package/types/core/StyleSheetManager.d.ts +16 -0
- package/types/entries/cookie-only.d.ts +8 -0
- package/types/entries/dom-only.d.ts +9 -0
- package/types/entries/dom-with-overrides-full.d.ts +15 -0
- package/types/entries/dom-with-overrides-lite.d.ts +11 -0
- package/types/entries/overrides-full-only.d.ts +11 -0
- package/types/entries/overrides-lite-only.d.ts +8 -0
- package/types/entries/vitals-only.d.ts +8 -0
- package/types/index.d.ts +13 -0
- package/types/overrides/BrowserCookieAdapter.d.ts +6 -0
- package/types/overrides/OverridesPlugin.d.ts +33 -0
- package/types/overrides/OverridesPluginFull.d.ts +9 -0
- package/types/overrides/OverridesPluginLite.d.ts +27 -0
- package/types/overrides/index.d.ts +3 -0
- package/types/overrides/types.d.ts +43 -0
- package/types/parsers/VariantExtractor.d.ts +49 -0
- package/types/types/index.d.ts +177 -0
- package/types/utils/URLMatcher.d.ts +20 -0
- package/types/utils/debug.d.ts +41 -0
- package/types/vitals/WebVitalsPlugin.d.ts +37 -0
- package/types/vitals/index.d.ts +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
# ABsmartly SDK Plugins
|
|
2
|
+
|
|
3
|
+
A comprehensive collection of plugins for the ABsmartly JavaScript SDK including DOM manipulation, experiment overrides, cookie management, and web vitals tracking.
|
|
4
|
+
|
|
5
|
+
## Available Plugins
|
|
6
|
+
|
|
7
|
+
### 1. DOMChangesPlugin
|
|
8
|
+
- **Complete DOM Manipulation**: All change types including styleRules with pseudo-states
|
|
9
|
+
- **Smart Exposure Tracking**: Cross-variant tracking prevents sample ratio mismatch
|
|
10
|
+
- **Dynamic Content Support**: Pending changes with `waitForElement` for SPAs
|
|
11
|
+
- **React/Vue Compatibility**: StyleRules survive re-renders
|
|
12
|
+
- **Browser Extension Integration**: Two-way communication for visual editing
|
|
13
|
+
|
|
14
|
+
### 2. OverridesPlugin
|
|
15
|
+
- **Query String Overrides**: Force experiments via URL parameters
|
|
16
|
+
- **Cookie Persistence**: Server-compatible experiment overrides
|
|
17
|
+
- **API Integration**: Fetch non-running experiments (Full version)
|
|
18
|
+
- **Development Support**: Test experiments in any environment
|
|
19
|
+
|
|
20
|
+
### 3. CookiePlugin
|
|
21
|
+
- **Unit ID Management**: Generate and persist user identifiers
|
|
22
|
+
- **UTM Tracking**: Capture and store UTM parameters
|
|
23
|
+
- **Landing Page Tracking**: Track first visits and referrers
|
|
24
|
+
- **Storage Fallbacks**: Cookie → localStorage → memory
|
|
25
|
+
|
|
26
|
+
### 4. WebVitalsPlugin
|
|
27
|
+
- **Core Web Vitals**: Track CLS, LCP, FCP, INP, TTFB
|
|
28
|
+
- **Page Metrics**: Network timing, DOM processing, resource counts
|
|
29
|
+
- **Performance Ratings**: Automatic good/needs-improvement/poor classification
|
|
30
|
+
- **Compression Metrics**: Track page size and compression ratios
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @absmartly/sdk-plugins
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### Basic Usage
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { DOMChangesPlugin, CookiePlugin, WebVitalsPlugin } from '@absmartly/sdk-plugins';
|
|
44
|
+
import absmartly from '@absmartly/javascript-sdk';
|
|
45
|
+
|
|
46
|
+
// Initialize ABsmartly SDK
|
|
47
|
+
const sdk = new absmartly.SDK({
|
|
48
|
+
endpoint: 'https://your-endpoint.absmartly.io/v1',
|
|
49
|
+
apiKey: 'YOUR_API_KEY',
|
|
50
|
+
environment: 'production',
|
|
51
|
+
application: 'your-app'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Create context
|
|
55
|
+
const context = sdk.createContext(request);
|
|
56
|
+
|
|
57
|
+
// Initialize plugins
|
|
58
|
+
const domPlugin = new DOMChangesPlugin({
|
|
59
|
+
context: context,
|
|
60
|
+
autoApply: true, // Automatically apply changes on init
|
|
61
|
+
spa: true, // Enable SPA support
|
|
62
|
+
visibilityTracking: true, // Track when changes become visible
|
|
63
|
+
extensionBridge: true, // Enable browser extension communication
|
|
64
|
+
dataSource: 'variable', // Use variables (or 'customField')
|
|
65
|
+
dataFieldName: '__dom_changes', // Variable/field name for DOM changes
|
|
66
|
+
debug: true // Enable debug logging
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Initialize without blocking
|
|
70
|
+
domPlugin.ready().then(() => {
|
|
71
|
+
console.log('DOMChangesPlugin ready');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Initialize cookie management
|
|
75
|
+
const cookiePlugin = new CookiePlugin({
|
|
76
|
+
context: context,
|
|
77
|
+
cookieDomain: '.yourdomain.com',
|
|
78
|
+
autoUpdateExpiry: true
|
|
79
|
+
});
|
|
80
|
+
cookiePlugin.ready().then(() => {
|
|
81
|
+
console.log('CookiePlugin ready');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Initialize web vitals tracking
|
|
85
|
+
const vitalsPlugin = new WebVitalsPlugin({
|
|
86
|
+
context: context,
|
|
87
|
+
trackWebVitals: true,
|
|
88
|
+
trackPageMetrics: true
|
|
89
|
+
});
|
|
90
|
+
vitalsPlugin.ready().then(() => {
|
|
91
|
+
console.log('WebVitalsPlugin ready');
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### With Experiment Overrides (Browser Extension Support)
|
|
96
|
+
|
|
97
|
+
The OverridesPlugin enables experiment overrides for internal testing and the ABsmartly Browser Extension. Simply load it before SDK initialization and it will automatically check for and apply any overrides:
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
import {
|
|
101
|
+
DOMChangesPlugin,
|
|
102
|
+
OverridesPlugin
|
|
103
|
+
} from '@absmartly/dom-changes-plugin';
|
|
104
|
+
import absmartly from '@absmartly/javascript-sdk';
|
|
105
|
+
|
|
106
|
+
// Initialize SDK and create context
|
|
107
|
+
const sdk = new absmartly.SDK({ /* ... */ });
|
|
108
|
+
const context = sdk.createContext({ /* ... */ });
|
|
109
|
+
|
|
110
|
+
// Initialize OverridesPlugin - it will automatically check for overrides
|
|
111
|
+
const overridesPlugin = new OverridesPlugin({
|
|
112
|
+
context: context,
|
|
113
|
+
cookieName: 'absmartly_overrides',
|
|
114
|
+
useQueryString: true,
|
|
115
|
+
queryPrefix: '_exp_',
|
|
116
|
+
envParam: 'env',
|
|
117
|
+
persistQueryToCookie: true, // Save query overrides to cookie
|
|
118
|
+
sdkEndpoint: 'https://your-endpoint.absmartly.io',
|
|
119
|
+
debug: true
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
overridesPlugin.ready().then(() => {
|
|
123
|
+
console.log('OverridesPlugin ready - overrides applied if present');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Initialize DOMChangesPlugin for all experiments
|
|
127
|
+
const domPlugin = new DOMChangesPlugin({
|
|
128
|
+
context: context,
|
|
129
|
+
autoApply: true,
|
|
130
|
+
dataSource: 'variable',
|
|
131
|
+
dataFieldName: '__dom_changes',
|
|
132
|
+
debug: true
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
domPlugin.ready().then(() => {
|
|
136
|
+
console.log('DOMChangesPlugin ready');
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Override Configuration Options
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
const overridesPlugin = new OverridesPlugin({
|
|
144
|
+
context: context, // Required: ABsmartly context
|
|
145
|
+
|
|
146
|
+
// Cookie configuration
|
|
147
|
+
cookieName: 'absmartly_overrides', // Cookie name (omit to disable cookies)
|
|
148
|
+
cookieOptions: {
|
|
149
|
+
path: '/',
|
|
150
|
+
secure: true,
|
|
151
|
+
sameSite: 'Lax'
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Query string configuration
|
|
155
|
+
useQueryString: true, // Enable query string parsing (default: true on client)
|
|
156
|
+
queryPrefix: '_exp_', // Prefix for experiment params (default: '_exp_')
|
|
157
|
+
envParam: 'env', // Environment parameter name (default: 'env')
|
|
158
|
+
persistQueryToCookie: false, // Save query overrides to cookie (default: false)
|
|
159
|
+
|
|
160
|
+
// Endpoints
|
|
161
|
+
sdkEndpoint: 'https://...', // SDK endpoint (required if not in context)
|
|
162
|
+
absmartlyEndpoint: 'https://...', // API endpoint for fetching experiments
|
|
163
|
+
|
|
164
|
+
// Server-side configuration
|
|
165
|
+
url: req.url, // URL for server-side (Node.js)
|
|
166
|
+
cookieAdapter: customAdapter, // Custom cookie adapter for server-side
|
|
167
|
+
|
|
168
|
+
debug: true // Enable debug logging
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Query String Format (New)
|
|
173
|
+
|
|
174
|
+
Use individual query parameters with configurable prefix:
|
|
175
|
+
```
|
|
176
|
+
# Single experiment
|
|
177
|
+
https://example.com?_exp_button_color=1
|
|
178
|
+
|
|
179
|
+
# Multiple experiments
|
|
180
|
+
https://example.com?_exp_hero_title=0&_exp_nav_style=2
|
|
181
|
+
|
|
182
|
+
# With environment
|
|
183
|
+
https://example.com?env=staging&_exp_dev_feature=1,1
|
|
184
|
+
|
|
185
|
+
# With experiment ID
|
|
186
|
+
https://example.com?_exp_archived_test=1,2,12345
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Cookie Format
|
|
190
|
+
|
|
191
|
+
Cookies use comma as separator (no encoding needed):
|
|
192
|
+
```javascript
|
|
193
|
+
// Simple overrides (comma-separated experiments)
|
|
194
|
+
document.cookie = 'absmartly_overrides=exp1:1,exp2:0';
|
|
195
|
+
|
|
196
|
+
// With environment flags (dot-separated values)
|
|
197
|
+
document.cookie = 'absmartly_overrides=exp1:1.0,exp2:0.1';
|
|
198
|
+
|
|
199
|
+
// With experiment ID
|
|
200
|
+
document.cookie = 'absmartly_overrides=exp1:1.2.12345';
|
|
201
|
+
|
|
202
|
+
// With dev environment
|
|
203
|
+
document.cookie = 'absmartly_overrides=devEnv=staging|exp1:1.1,exp2:0.1';
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Format**: `name:variant[.env][.id]` where:
|
|
207
|
+
- Experiments are separated by `,` (comma)
|
|
208
|
+
- Values within an experiment are separated by `.` (dot)
|
|
209
|
+
- Environment prefix uses `|` (pipe) separator
|
|
210
|
+
|
|
211
|
+
#### How Overrides Work
|
|
212
|
+
|
|
213
|
+
1. **Query String Priority**: Query parameters take precedence over cookies
|
|
214
|
+
2. **Environment Support**: Use `env` parameter for dev/staging experiments
|
|
215
|
+
3. **API Fetching**: Non-running experiments are fetched from ABsmartly API
|
|
216
|
+
4. **Context Injection**: Experiments are transparently injected into context.data()
|
|
217
|
+
5. **DOM Application**: DOMChangesPlugin applies changes from all experiments
|
|
218
|
+
|
|
219
|
+
## DOM Change Types
|
|
220
|
+
|
|
221
|
+
The plugin supports comprehensive DOM manipulation with advanced features:
|
|
222
|
+
|
|
223
|
+
### Core Change Types
|
|
224
|
+
|
|
225
|
+
### Text Change
|
|
226
|
+
```javascript
|
|
227
|
+
{
|
|
228
|
+
selector: '.headline',
|
|
229
|
+
type: 'text',
|
|
230
|
+
value: 'New Headline Text'
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### HTML Change
|
|
235
|
+
```javascript
|
|
236
|
+
{
|
|
237
|
+
selector: '.content',
|
|
238
|
+
type: 'html',
|
|
239
|
+
value: '<p>New <strong>HTML</strong> content</p>'
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Style Change (Inline)
|
|
244
|
+
```javascript
|
|
245
|
+
{
|
|
246
|
+
selector: '.button',
|
|
247
|
+
type: 'style',
|
|
248
|
+
value: {
|
|
249
|
+
backgroundColor: 'red', // Use camelCase for CSS properties
|
|
250
|
+
color: '#ffffff',
|
|
251
|
+
fontSize: '18px'
|
|
252
|
+
},
|
|
253
|
+
trigger_on_view: false // Control exposure timing
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Style Rules (With Pseudo-States) ⭐ NEW
|
|
258
|
+
```javascript
|
|
259
|
+
{
|
|
260
|
+
selector: '.button',
|
|
261
|
+
type: 'styleRules',
|
|
262
|
+
states: {
|
|
263
|
+
normal: {
|
|
264
|
+
backgroundColor: '#007bff',
|
|
265
|
+
color: 'white',
|
|
266
|
+
padding: '10px 20px',
|
|
267
|
+
borderRadius: '4px',
|
|
268
|
+
transition: 'all 0.2s ease'
|
|
269
|
+
},
|
|
270
|
+
hover: {
|
|
271
|
+
backgroundColor: '#0056b3',
|
|
272
|
+
transform: 'translateY(-2px)',
|
|
273
|
+
boxShadow: '0 4px 8px rgba(0,0,0,0.2)'
|
|
274
|
+
},
|
|
275
|
+
active: {
|
|
276
|
+
backgroundColor: '#004085',
|
|
277
|
+
transform: 'translateY(0)'
|
|
278
|
+
},
|
|
279
|
+
focus: {
|
|
280
|
+
outline: '2px solid #007bff',
|
|
281
|
+
outlineOffset: '2px'
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
important: true, // default is true
|
|
285
|
+
trigger_on_view: true
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Benefits of styleRules:**
|
|
290
|
+
- Handles CSS pseudo-states properly (hover, active, focus)
|
|
291
|
+
- Survives React re-renders through stylesheet injection
|
|
292
|
+
- More performant than inline styles for complex interactions
|
|
293
|
+
|
|
294
|
+
### Class Change
|
|
295
|
+
```javascript
|
|
296
|
+
{
|
|
297
|
+
selector: '.card',
|
|
298
|
+
type: 'class',
|
|
299
|
+
add: ['highlighted', 'featured'],
|
|
300
|
+
remove: ['default']
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Attribute Change
|
|
305
|
+
```javascript
|
|
306
|
+
{
|
|
307
|
+
selector: 'input[name="email"]',
|
|
308
|
+
type: 'attribute',
|
|
309
|
+
value: {
|
|
310
|
+
'placeholder': 'Enter your email',
|
|
311
|
+
'required': 'true'
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### JavaScript Execution
|
|
317
|
+
```javascript
|
|
318
|
+
{
|
|
319
|
+
selector: '.dynamic-element',
|
|
320
|
+
type: 'javascript',
|
|
321
|
+
value: 'element.addEventListener("click", () => console.log("Clicked!"))'
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Element Move
|
|
326
|
+
```javascript
|
|
327
|
+
{
|
|
328
|
+
selector: '.sidebar',
|
|
329
|
+
type: 'move',
|
|
330
|
+
targetSelector: '.main-content',
|
|
331
|
+
position: 'before' // 'before', 'after', 'firstChild', 'lastChild'
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Element Creation
|
|
336
|
+
```javascript
|
|
337
|
+
{
|
|
338
|
+
selector: '.new-banner', // For identification
|
|
339
|
+
type: 'create',
|
|
340
|
+
element: '<div class="banner">Special Offer!</div>',
|
|
341
|
+
targetSelector: 'body',
|
|
342
|
+
position: 'firstChild',
|
|
343
|
+
trigger_on_view: false
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Pending Changes (Elements Not Yet in DOM) ⭐ NEW
|
|
348
|
+
```javascript
|
|
349
|
+
{
|
|
350
|
+
selector: '.lazy-loaded-button',
|
|
351
|
+
type: 'style',
|
|
352
|
+
value: {
|
|
353
|
+
backgroundColor: 'red',
|
|
354
|
+
color: 'white'
|
|
355
|
+
},
|
|
356
|
+
waitForElement: true, // Wait for element to appear
|
|
357
|
+
observerRoot: '.main-content', // Optional: specific container to watch
|
|
358
|
+
trigger_on_view: true
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Perfect for:**
|
|
363
|
+
- Lazy-loaded content
|
|
364
|
+
- React components that mount/unmount
|
|
365
|
+
- Modal dialogs
|
|
366
|
+
- API-loaded content
|
|
367
|
+
- Infinite scroll items
|
|
368
|
+
|
|
369
|
+
## Key Features
|
|
370
|
+
|
|
371
|
+
### Exposure Tracking with trigger_on_view
|
|
372
|
+
|
|
373
|
+
The `trigger_on_view` property prevents sample ratio mismatch by controlling when A/B test exposures are recorded:
|
|
374
|
+
|
|
375
|
+
```javascript
|
|
376
|
+
{
|
|
377
|
+
selector: '.below-fold-element',
|
|
378
|
+
type: 'style',
|
|
379
|
+
value: { backgroundColor: 'blue' },
|
|
380
|
+
trigger_on_view: true // Only trigger when element becomes visible
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
- **`false` (default)**: Exposure triggers immediately
|
|
385
|
+
- **`true`**: Exposure triggers when element enters viewport
|
|
386
|
+
- **Cross-variant tracking**: Tracks elements from ALL variants for unbiased exposure
|
|
387
|
+
|
|
388
|
+
### Core API Methods
|
|
389
|
+
|
|
390
|
+
```javascript
|
|
391
|
+
// Apply changes from ABsmartly context
|
|
392
|
+
await plugin.applyChanges('experiment-name');
|
|
393
|
+
|
|
394
|
+
// Apply individual change
|
|
395
|
+
const success = plugin.applyChange(change, 'experiment-name');
|
|
396
|
+
|
|
397
|
+
// Remove changes
|
|
398
|
+
plugin.removeChanges('experiment-name');
|
|
399
|
+
|
|
400
|
+
// Get applied changes
|
|
401
|
+
const changes = plugin.getAppliedChanges('experiment-name');
|
|
402
|
+
|
|
403
|
+
// Clean up resources
|
|
404
|
+
plugin.destroy();
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Browser Extension Integration
|
|
408
|
+
|
|
409
|
+
The plugin communicates with the ABsmartly Browser Extension for visual editing:
|
|
410
|
+
|
|
411
|
+
- **Automatic Detection**: Detects and connects to the extension
|
|
412
|
+
- **Message Protocol**: Two-way communication for preview mode and visual editing
|
|
413
|
+
- **Preview Support**: Test changes before creating experiments
|
|
414
|
+
|
|
415
|
+
## Documentation
|
|
416
|
+
|
|
417
|
+
For detailed documentation:
|
|
418
|
+
|
|
419
|
+
- **[Extension Integration Guide](docs/EXTENSION_INTEGRATION_GUIDE.md)** - Complete guide for browser extension integration, visual editing, preview mode, and advanced features
|
|
420
|
+
- **[Exposure Tracking Guide](docs/EXPOSURE_TRACKING_GUIDE.md)** - Understanding trigger_on_view and preventing sample ratio mismatch
|
|
421
|
+
|
|
422
|
+
## Anti-Flicker Support
|
|
423
|
+
|
|
424
|
+
Prevent content flash before experiments load with two modes:
|
|
425
|
+
|
|
426
|
+
### Quick Start
|
|
427
|
+
|
|
428
|
+
**Hide entire page with smooth fade-in:**
|
|
429
|
+
```javascript
|
|
430
|
+
const domPlugin = new DOMChangesPlugin({
|
|
431
|
+
context: context,
|
|
432
|
+
hideUntilReady: 'body',
|
|
433
|
+
hideTransition: '0.3s ease-in' // Smooth fade-in
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Hide only experiment elements (recommended):**
|
|
438
|
+
```html
|
|
439
|
+
<!-- Mark elements that need anti-flicker -->
|
|
440
|
+
<div data-absmartly-hide>
|
|
441
|
+
Hero section with experiment
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
<div data-absmartly-hide>
|
|
445
|
+
CTA button being tested
|
|
446
|
+
</div>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
const domPlugin = new DOMChangesPlugin({
|
|
451
|
+
context: context,
|
|
452
|
+
hideUntilReady: 'elements', // Hide only marked elements
|
|
453
|
+
hideTransition: '0.4s ease-out'
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### How It Works
|
|
458
|
+
|
|
459
|
+
1. **During Load (Before Experiments Applied):**
|
|
460
|
+
```css
|
|
461
|
+
/* No transition: */
|
|
462
|
+
body { visibility: hidden !important; }
|
|
463
|
+
|
|
464
|
+
/* With transition: */
|
|
465
|
+
body {
|
|
466
|
+
visibility: hidden !important;
|
|
467
|
+
opacity: 0 !important;
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
2. **After Experiments Applied:**
|
|
472
|
+
- **No transition**: Instant reveal (removes `visibility:hidden`)
|
|
473
|
+
- **With transition**: Smooth 4-step fade-in:
|
|
474
|
+
1. Remove `visibility:hidden`
|
|
475
|
+
2. Add CSS transition
|
|
476
|
+
3. Animate opacity 0 → 1
|
|
477
|
+
4. Clean up after transition completes
|
|
478
|
+
|
|
479
|
+
### Configuration Options
|
|
480
|
+
|
|
481
|
+
```javascript
|
|
482
|
+
new DOMChangesPlugin({
|
|
483
|
+
// Anti-flicker configuration
|
|
484
|
+
hideUntilReady: 'body', // 'body' | 'elements' | true | false
|
|
485
|
+
// 'body': hide entire page
|
|
486
|
+
// 'elements': hide only marked elements
|
|
487
|
+
// true: same as 'elements'
|
|
488
|
+
// false: disabled (default)
|
|
489
|
+
|
|
490
|
+
hideTimeout: 3000, // Max wait time in ms (default: 3000)
|
|
491
|
+
// Content auto-shows after timeout
|
|
492
|
+
// even if experiments fail to load
|
|
493
|
+
|
|
494
|
+
hideTransition: '0.3s ease-in', // CSS transition for fade-in
|
|
495
|
+
// Examples: '0.3s ease-in', '0.5s linear'
|
|
496
|
+
// false: instant reveal (default)
|
|
497
|
+
|
|
498
|
+
hideSelector: '[data-absmartly-hide]' // Custom selector for 'elements' mode
|
|
499
|
+
// default: '[data-absmartly-hide]'
|
|
500
|
+
})
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Why This Matters
|
|
504
|
+
|
|
505
|
+
**Without anti-flicker:**
|
|
506
|
+
```
|
|
507
|
+
User sees: Original → FLASH → Experiment Version
|
|
508
|
+
^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^^
|
|
509
|
+
(bad UX) (jarring)
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**With anti-flicker:**
|
|
513
|
+
```
|
|
514
|
+
User sees: [Hidden] → Experiment Version (smooth fade-in)
|
|
515
|
+
^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
516
|
+
(no flash) (professional)
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Best Practices
|
|
520
|
+
|
|
521
|
+
**✅ Use `hideUntilReady: 'elements'` (Recommended)**
|
|
522
|
+
- Mark only elements being tested
|
|
523
|
+
- Faster perceived load time
|
|
524
|
+
- Better user experience
|
|
525
|
+
- No CLS (Cumulative Layout Shift)
|
|
526
|
+
|
|
527
|
+
```html
|
|
528
|
+
<nav>Normal navigation</nav>
|
|
529
|
+
<div data-absmartly-hide>
|
|
530
|
+
<!-- Only this hero is hidden -->
|
|
531
|
+
<h1>Hero headline being tested</h1>
|
|
532
|
+
</div>
|
|
533
|
+
<main>Rest of content visible immediately</main>
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**✅ Use `hideUntilReady: 'body'` (For whole-page experiments)**
|
|
537
|
+
- Complete redesigns
|
|
538
|
+
- Multiple changes across page
|
|
539
|
+
- Ensures zero flicker
|
|
540
|
+
|
|
541
|
+
**✅ Set appropriate timeout**
|
|
542
|
+
```javascript
|
|
543
|
+
{
|
|
544
|
+
hideTimeout: 2000 // Faster for good connections
|
|
545
|
+
hideTimeout: 3000 // Balanced (default)
|
|
546
|
+
hideTimeout: 5000 // Safer for slow connections
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**⚠️ Avoid long transitions**
|
|
551
|
+
```javascript
|
|
552
|
+
hideTransition: '0.2s ease-in' // ✅ Subtle, professional
|
|
553
|
+
hideTransition: '0.3s ease-out' // ✅ Smooth
|
|
554
|
+
hideTransition: '1s linear' // ❌ Too slow, feels broken
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Industry Standard Comparison
|
|
558
|
+
|
|
559
|
+
| Tool | Method | Our Implementation |
|
|
560
|
+
|------|--------|-------------------|
|
|
561
|
+
| Google Optimize | `opacity: 0` | ✅ `visibility:hidden` (better) |
|
|
562
|
+
| Optimizely | `opacity: 0` | ✅ `visibility:hidden` (better) |
|
|
563
|
+
| VWO | `opacity: 0` on body | ✅ Both modes supported |
|
|
564
|
+
| **ABsmartly** | - | ✅ `visibility:hidden` + optional smooth fade |
|
|
565
|
+
|
|
566
|
+
**Why `visibility:hidden` is better:**
|
|
567
|
+
- ✅ Hides from screen readers
|
|
568
|
+
- ✅ Prevents interaction
|
|
569
|
+
- ✅ No CLS (Cumulative Layout Shift)
|
|
570
|
+
- ✅ Can add smooth opacity transition
|
|
571
|
+
|
|
572
|
+
## Style Persistence
|
|
573
|
+
|
|
574
|
+
Automatically reapply experiment styles when frameworks (React/Vue/Angular) overwrite them:
|
|
575
|
+
|
|
576
|
+
### The Problem
|
|
577
|
+
|
|
578
|
+
```javascript
|
|
579
|
+
// Experiment changes button to red
|
|
580
|
+
{ selector: '.button', type: 'style', value: { backgroundColor: 'red' } }
|
|
581
|
+
|
|
582
|
+
// User hovers → React's hover handler changes it back to blue
|
|
583
|
+
// Experiment style is lost! ❌
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### The Solution
|
|
587
|
+
|
|
588
|
+
```javascript
|
|
589
|
+
{
|
|
590
|
+
selector: '.button',
|
|
591
|
+
type: 'style',
|
|
592
|
+
value: { backgroundColor: 'red' },
|
|
593
|
+
persistStyle: true // ✅ Reapply when React overwrites it
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### How It Works
|
|
598
|
+
|
|
599
|
+
1. **MutationObserver** watches for style attribute changes
|
|
600
|
+
2. **Detects** when framework overwrites experiment styles
|
|
601
|
+
3. **Automatically reapplies** experiment styles
|
|
602
|
+
4. **Throttled logging** (max once per 5 seconds to avoid spam)
|
|
603
|
+
|
|
604
|
+
### When It's Enabled
|
|
605
|
+
|
|
606
|
+
**Automatically in SPA mode:**
|
|
607
|
+
```javascript
|
|
608
|
+
new DOMChangesPlugin({
|
|
609
|
+
context: context,
|
|
610
|
+
spa: true // ✅ Style persistence auto-enabled for all style changes
|
|
611
|
+
})
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**Opt-in per change:**
|
|
615
|
+
```javascript
|
|
616
|
+
{
|
|
617
|
+
selector: '.button',
|
|
618
|
+
type: 'style',
|
|
619
|
+
value: { backgroundColor: 'red' },
|
|
620
|
+
persistStyle: true // ✅ Explicit opt-in (works even if spa: false)
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Use Cases
|
|
625
|
+
|
|
626
|
+
**✅ React components with hover states:**
|
|
627
|
+
```javascript
|
|
628
|
+
// Button has React onClick that changes style
|
|
629
|
+
{
|
|
630
|
+
selector: '.cta-button',
|
|
631
|
+
type: 'style',
|
|
632
|
+
value: { backgroundColor: '#ff6b6b' },
|
|
633
|
+
persistStyle: true // Survives React re-renders
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**✅ Vue reactive styles:**
|
|
638
|
+
```javascript
|
|
639
|
+
{
|
|
640
|
+
selector: '.dynamic-price',
|
|
641
|
+
type: 'style',
|
|
642
|
+
value: { color: 'green', fontWeight: 'bold' },
|
|
643
|
+
persistStyle: true // Persists through Vue updates
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**✅ Angular material components:**
|
|
648
|
+
```javascript
|
|
649
|
+
{
|
|
650
|
+
selector: 'mat-button',
|
|
651
|
+
type: 'style',
|
|
652
|
+
value: { backgroundColor: '#1976d2' },
|
|
653
|
+
persistStyle: true // Works with Angular's style binding
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**❌ Static HTML (persistence not needed):**
|
|
658
|
+
```javascript
|
|
659
|
+
{
|
|
660
|
+
selector: '.static-banner',
|
|
661
|
+
type: 'style',
|
|
662
|
+
value: { display: 'none' },
|
|
663
|
+
persistStyle: false // Not needed, saves performance
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
## URL Filtering
|
|
668
|
+
|
|
669
|
+
Target experiments to specific pages or URL patterns using the `urlFilter` property. This enables precise control over where experiments run without code changes.
|
|
670
|
+
|
|
671
|
+
### Basic URL Filtering
|
|
672
|
+
|
|
673
|
+
Wrap your DOM changes in a configuration object with `urlFilter`:
|
|
674
|
+
|
|
675
|
+
```javascript
|
|
676
|
+
{
|
|
677
|
+
urlFilter: '/products', // Simple path match
|
|
678
|
+
changes: [
|
|
679
|
+
{
|
|
680
|
+
selector: '.product-title',
|
|
681
|
+
type: 'style',
|
|
682
|
+
value: { color: 'red' }
|
|
683
|
+
}
|
|
684
|
+
]
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### URL Filter Configuration
|
|
689
|
+
|
|
690
|
+
```javascript
|
|
691
|
+
{
|
|
692
|
+
urlFilter: {
|
|
693
|
+
include: ['/checkout', '/cart'], // Run on these URLs
|
|
694
|
+
exclude: ['/checkout/success'], // But NOT on these
|
|
695
|
+
mode: 'simple', // 'simple' or 'regex'
|
|
696
|
+
matchType: 'path' // 'full-url', 'path', 'domain', 'query', 'hash'
|
|
697
|
+
},
|
|
698
|
+
changes: [ /* ... */ ]
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Match Types
|
|
703
|
+
|
|
704
|
+
**`path` (default)** - Match against pathname + hash:
|
|
705
|
+
```javascript
|
|
706
|
+
// URL: https://example.com/products/shoes#new
|
|
707
|
+
// Matches: "/products/shoes#new"
|
|
708
|
+
{
|
|
709
|
+
urlFilter: { include: ['/products/*'], matchType: 'path' },
|
|
710
|
+
changes: [ /* ... */ ]
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
**`full-url`** - Match complete URL including protocol and domain:
|
|
715
|
+
```javascript
|
|
716
|
+
// URL: https://example.com/products
|
|
717
|
+
// Matches: "https://example.com/products"
|
|
718
|
+
{
|
|
719
|
+
urlFilter: { include: ['https://example.com/products'], matchType: 'full-url' },
|
|
720
|
+
changes: [ /* ... */ ]
|
|
721
|
+
}
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**`domain`** - Match only the hostname:
|
|
725
|
+
```javascript
|
|
726
|
+
// URL: https://shop.example.com/products
|
|
727
|
+
// Matches: "shop.example.com"
|
|
728
|
+
{
|
|
729
|
+
urlFilter: { include: ['shop.example.com'], matchType: 'domain' },
|
|
730
|
+
changes: [ /* ... */ ]
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
**`query`** - Match only query parameters:
|
|
735
|
+
```javascript
|
|
736
|
+
// URL: https://example.com/products?category=shoes
|
|
737
|
+
// Matches: "?category=shoes"
|
|
738
|
+
{
|
|
739
|
+
urlFilter: { include: ['*category=shoes*'], matchType: 'query' },
|
|
740
|
+
changes: [ /* ... */ ]
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
**`hash`** - Match only hash fragment:
|
|
745
|
+
```javascript
|
|
746
|
+
// URL: https://example.com/page#section-1
|
|
747
|
+
// Matches: "#section-1"
|
|
748
|
+
{
|
|
749
|
+
urlFilter: { include: ['#section-*'], matchType: 'hash' },
|
|
750
|
+
changes: [ /* ... */ ]
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Pattern Matching Modes
|
|
755
|
+
|
|
756
|
+
**Simple Mode (default)** - Glob-style wildcards:
|
|
757
|
+
```javascript
|
|
758
|
+
{
|
|
759
|
+
urlFilter: {
|
|
760
|
+
include: [
|
|
761
|
+
'/products/*', // All product pages
|
|
762
|
+
'/checkout*', // Checkout and checkout/*
|
|
763
|
+
'/about', // Exact match
|
|
764
|
+
'*/special-offer' // Any path ending with /special-offer
|
|
765
|
+
],
|
|
766
|
+
mode: 'simple'
|
|
767
|
+
},
|
|
768
|
+
changes: [ /* ... */ ]
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
**Simple Mode Wildcards:**
|
|
773
|
+
- `*` = Match any characters (0 or more)
|
|
774
|
+
- `?` = Match single character
|
|
775
|
+
- No wildcards = Exact match
|
|
776
|
+
|
|
777
|
+
**Regex Mode** - Full regex power:
|
|
778
|
+
```javascript
|
|
779
|
+
{
|
|
780
|
+
urlFilter: {
|
|
781
|
+
include: [
|
|
782
|
+
'^/products/(shoes|bags)', // Products in specific categories
|
|
783
|
+
'/checkout/(step-[1-3])', // Checkout steps 1-3 only
|
|
784
|
+
'\\?utm_source=email' // URLs with email traffic
|
|
785
|
+
],
|
|
786
|
+
mode: 'regex'
|
|
787
|
+
},
|
|
788
|
+
changes: [ /* ... */ ]
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### Include and Exclude Logic
|
|
793
|
+
|
|
794
|
+
**Include only:**
|
|
795
|
+
```javascript
|
|
796
|
+
{
|
|
797
|
+
urlFilter: { include: ['/products/*'] },
|
|
798
|
+
changes: [ /* ... */ ]
|
|
799
|
+
}
|
|
800
|
+
// ✅ Runs on: /products/shoes, /products/bags
|
|
801
|
+
// ❌ Skips: /about, /checkout
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
**Include with exclusions:**
|
|
805
|
+
```javascript
|
|
806
|
+
{
|
|
807
|
+
urlFilter: {
|
|
808
|
+
include: ['/products/*'],
|
|
809
|
+
exclude: ['/products/admin', '/products/*/edit']
|
|
810
|
+
},
|
|
811
|
+
changes: [ /* ... */ ]
|
|
812
|
+
}
|
|
813
|
+
// ✅ Runs on: /products/shoes, /products/bags
|
|
814
|
+
// ❌ Skips: /products/admin, /products/shoes/edit
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
**Exclude only (match all except):**
|
|
818
|
+
```javascript
|
|
819
|
+
{
|
|
820
|
+
urlFilter: { exclude: ['/admin/*', '/api/*'] },
|
|
821
|
+
changes: [ /* ... */ ]
|
|
822
|
+
}
|
|
823
|
+
// ✅ Runs on: /products, /checkout, /about
|
|
824
|
+
// ❌ Skips: /admin/users, /api/data
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
**No filter (match all):**
|
|
828
|
+
```javascript
|
|
829
|
+
{
|
|
830
|
+
urlFilter: {}, // or omit urlFilter entirely
|
|
831
|
+
changes: [ /* ... */ ]
|
|
832
|
+
}
|
|
833
|
+
// ✅ Runs on all pages
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
**Match nothing (disable experiment):**
|
|
837
|
+
```javascript
|
|
838
|
+
{
|
|
839
|
+
urlFilter: { include: [] }, // Empty array = explicit "match nothing"
|
|
840
|
+
changes: [ /* ... */ ]
|
|
841
|
+
}
|
|
842
|
+
// ❌ Never runs (useful for temporary disabling)
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
### SRM Prevention with URL Filters
|
|
846
|
+
|
|
847
|
+
**Critical:** When using `urlFilter`, the plugin implements **Sample Ratio Mismatch (SRM) prevention** by tracking ALL variants, not just the current variant:
|
|
848
|
+
|
|
849
|
+
```javascript
|
|
850
|
+
// Variant 0 (Control): No URL filter
|
|
851
|
+
{
|
|
852
|
+
changes: [
|
|
853
|
+
{ selector: '.hero', type: 'text', value: 'Welcome' }
|
|
854
|
+
]
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Variant 1: Only runs on /products
|
|
858
|
+
{
|
|
859
|
+
urlFilter: '/products',
|
|
860
|
+
changes: [
|
|
861
|
+
{ selector: '.hero', type: 'text', value: 'Shop Now' }
|
|
862
|
+
]
|
|
863
|
+
}
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**Without SRM prevention:**
|
|
867
|
+
- Users on `/products` → Tracked in both variants ✅
|
|
868
|
+
- Users on `/about` → Only tracked in variant 0 ❌
|
|
869
|
+
- **Result:** Biased sample sizes!
|
|
870
|
+
|
|
871
|
+
**With SRM prevention (automatic):**
|
|
872
|
+
- Users on `/products` → Tracked in both variants ✅
|
|
873
|
+
- Users on `/about` → Tracked in both variants ✅
|
|
874
|
+
- **Result:** Unbiased sample sizes! ✅
|
|
875
|
+
|
|
876
|
+
The plugin checks if **ANY variant** matches the current URL. If ANY variant matches, ALL variants are tracked for that user, ensuring fair sample distribution.
|
|
877
|
+
|
|
878
|
+
### URL Change Detection (SPA Mode)
|
|
879
|
+
|
|
880
|
+
In SPA mode (`spa: true`), the plugin automatically detects URL changes and re-evaluates experiments:
|
|
881
|
+
|
|
882
|
+
```javascript
|
|
883
|
+
new DOMChangesPlugin({
|
|
884
|
+
context: context,
|
|
885
|
+
spa: true, // Enable URL change detection
|
|
886
|
+
autoApply: true
|
|
887
|
+
});
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
**What it tracks:**
|
|
891
|
+
- ✅ `history.pushState()` - Client-side navigation
|
|
892
|
+
- ✅ `history.replaceState()` - URL updates
|
|
893
|
+
- ✅ `popstate` events - Browser back/forward buttons
|
|
894
|
+
|
|
895
|
+
**What happens on URL change:**
|
|
896
|
+
1. Remove all currently applied DOM changes
|
|
897
|
+
2. Re-evaluate URL filters with the new URL
|
|
898
|
+
3. Apply changes for experiments matching the new URL
|
|
899
|
+
4. Re-track exposure for newly applied experiments
|
|
900
|
+
|
|
901
|
+
**Example flow:**
|
|
902
|
+
```
|
|
903
|
+
User lands on: /products → Apply product page experiments
|
|
904
|
+
User navigates to: /checkout → Remove product experiments, apply checkout experiments
|
|
905
|
+
User goes back: /products → Remove checkout experiments, re-apply product experiments
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
**Without SPA mode:**
|
|
909
|
+
- URL changes are NOT detected
|
|
910
|
+
- Experiments applied on initial page load remain active
|
|
911
|
+
- New experiments on the new URL are NOT applied
|
|
912
|
+
- Use `spa: false` only for static sites without client-side routing
|
|
913
|
+
|
|
914
|
+
### Complete Examples
|
|
915
|
+
|
|
916
|
+
**E-commerce experiment on specific pages:**
|
|
917
|
+
```javascript
|
|
918
|
+
{
|
|
919
|
+
urlFilter: {
|
|
920
|
+
include: ['/products/*', '/collections/*'],
|
|
921
|
+
exclude: ['/products/admin', '*/edit'],
|
|
922
|
+
matchType: 'path',
|
|
923
|
+
mode: 'simple'
|
|
924
|
+
},
|
|
925
|
+
changes: [
|
|
926
|
+
{
|
|
927
|
+
selector: '.add-to-cart-button',
|
|
928
|
+
type: 'style',
|
|
929
|
+
value: { backgroundColor: '#ff6b6b', color: 'white' },
|
|
930
|
+
trigger_on_view: true
|
|
931
|
+
}
|
|
932
|
+
]
|
|
933
|
+
}
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
**Blog post experiment with regex:**
|
|
937
|
+
```javascript
|
|
938
|
+
{
|
|
939
|
+
urlFilter: {
|
|
940
|
+
include: ['^/blog/20(24|25)'], // Only 2024-2025 blog posts
|
|
941
|
+
mode: 'regex',
|
|
942
|
+
matchType: 'path'
|
|
943
|
+
},
|
|
944
|
+
changes: [
|
|
945
|
+
{
|
|
946
|
+
selector: '.blog-cta',
|
|
947
|
+
type: 'text',
|
|
948
|
+
value: 'Subscribe to our newsletter!'
|
|
949
|
+
}
|
|
950
|
+
]
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**Landing page experiment with UTM parameters:**
|
|
955
|
+
```javascript
|
|
956
|
+
{
|
|
957
|
+
urlFilter: {
|
|
958
|
+
include: ['*utm_campaign=summer*'],
|
|
959
|
+
matchType: 'query'
|
|
960
|
+
},
|
|
961
|
+
changes: [
|
|
962
|
+
{
|
|
963
|
+
selector: '.hero-banner',
|
|
964
|
+
type: 'html',
|
|
965
|
+
value: '<h1>Summer Sale - 50% Off!</h1>'
|
|
966
|
+
}
|
|
967
|
+
]
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
**Homepage only (exact match):**
|
|
972
|
+
```javascript
|
|
973
|
+
{
|
|
974
|
+
urlFilter: '/', // Shorthand for { include: ['/'] }
|
|
975
|
+
changes: [
|
|
976
|
+
{
|
|
977
|
+
selector: '.hero-title',
|
|
978
|
+
type: 'text',
|
|
979
|
+
value: 'Limited Time Offer!'
|
|
980
|
+
}
|
|
981
|
+
]
|
|
982
|
+
}
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
**Multiple pages (array shorthand):**
|
|
986
|
+
```javascript
|
|
987
|
+
{
|
|
988
|
+
urlFilter: ['/pricing', '/features'], // Shorthand for { include: [...] }
|
|
989
|
+
changes: [ /* ... */ ]
|
|
990
|
+
}
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
## SPA Mode (React/Vue/Angular Support)
|
|
994
|
+
|
|
995
|
+
Enable `spa: true` for Single Page Applications. This automatically activates three critical features:
|
|
996
|
+
|
|
997
|
+
### 1. URL Change Detection
|
|
998
|
+
|
|
999
|
+
Automatically detects client-side navigation and re-evaluates experiments:
|
|
1000
|
+
|
|
1001
|
+
- Intercepts `history.pushState()` and `history.replaceState()`
|
|
1002
|
+
- Listens to `popstate` events (browser back/forward)
|
|
1003
|
+
- Re-applies appropriate experiments when URL changes
|
|
1004
|
+
|
|
1005
|
+
See [URL Change Detection](#url-change-detection-spa-mode) above for details.
|
|
1006
|
+
|
|
1007
|
+
### 2. Wait for Element (Lazy-Loaded Content)
|
|
1008
|
+
|
|
1009
|
+
Automatically waits for elements that don't exist yet in the DOM:
|
|
1010
|
+
|
|
1011
|
+
```javascript
|
|
1012
|
+
{
|
|
1013
|
+
selector: '.lazy-loaded-button',
|
|
1014
|
+
type: 'style',
|
|
1015
|
+
value: { backgroundColor: 'red' }
|
|
1016
|
+
// No waitForElement needed - SPA mode handles it automatically!
|
|
1017
|
+
}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
**Without SPA mode:** Change skipped if element doesn't exist
|
|
1021
|
+
**With SPA mode:** Observes DOM and applies when element appears
|
|
1022
|
+
|
|
1023
|
+
### 3. Style Persistence (Framework Conflicts)
|
|
1024
|
+
|
|
1025
|
+
Automatically re-applies styles when frameworks overwrite them:
|
|
1026
|
+
|
|
1027
|
+
```javascript
|
|
1028
|
+
{
|
|
1029
|
+
selector: '.button',
|
|
1030
|
+
type: 'style',
|
|
1031
|
+
value: { backgroundColor: 'red' }
|
|
1032
|
+
// No persistStyle needed - SPA mode handles it automatically!
|
|
1033
|
+
}
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
**Without SPA mode:** React hover states can overwrite experiment styles
|
|
1037
|
+
**With SPA mode:** Detects and re-applies styles automatically
|
|
1038
|
+
|
|
1039
|
+
### When to Use SPA Mode
|
|
1040
|
+
|
|
1041
|
+
**✅ Enable `spa: true` for:**
|
|
1042
|
+
- React applications
|
|
1043
|
+
- Vue.js applications
|
|
1044
|
+
- Angular applications
|
|
1045
|
+
- Any app with client-side routing
|
|
1046
|
+
- Apps with lazy-loaded components
|
|
1047
|
+
- Apps with dynamic DOM updates
|
|
1048
|
+
|
|
1049
|
+
**❌ Keep `spa: false` for:**
|
|
1050
|
+
- Static HTML sites
|
|
1051
|
+
- Server-rendered pages without client-side routing
|
|
1052
|
+
- Sites where performance is critical and you don't need dynamic features
|
|
1053
|
+
|
|
1054
|
+
### Manual Override
|
|
1055
|
+
|
|
1056
|
+
You can still use flags explicitly if you need granular control:
|
|
1057
|
+
|
|
1058
|
+
```javascript
|
|
1059
|
+
{
|
|
1060
|
+
selector: '.button',
|
|
1061
|
+
type: 'style',
|
|
1062
|
+
value: { backgroundColor: 'red' },
|
|
1063
|
+
persistStyle: true, // Force enable (even if spa: false)
|
|
1064
|
+
waitForElement: true // Force enable (even if spa: false)
|
|
1065
|
+
}
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
## Configuration Options
|
|
1069
|
+
|
|
1070
|
+
```javascript
|
|
1071
|
+
new DOMChangesPlugin({
|
|
1072
|
+
// Required
|
|
1073
|
+
context: absmartlyContext, // ABsmartly context
|
|
1074
|
+
|
|
1075
|
+
// Core options
|
|
1076
|
+
autoApply: true, // Auto-apply changes from SDK
|
|
1077
|
+
spa: true, // Enable SPA support
|
|
1078
|
+
// ✅ Auto-enables: waitForElement + persistStyle
|
|
1079
|
+
visibilityTracking: true, // Track viewport visibility
|
|
1080
|
+
extensionBridge: true, // Enable browser extension communication
|
|
1081
|
+
dataSource: 'variable', // 'variable' or 'customField'
|
|
1082
|
+
dataFieldName: '__dom_changes', // Variable/field name for changes
|
|
1083
|
+
debug: false, // Enable debug logging
|
|
1084
|
+
|
|
1085
|
+
// Anti-flicker options
|
|
1086
|
+
hideUntilReady: false, // 'body' | 'elements' | true | false
|
|
1087
|
+
hideTimeout: 3000, // Max wait time (ms)
|
|
1088
|
+
hideTransition: false, // CSS transition (e.g., '0.3s ease-in') or false
|
|
1089
|
+
hideSelector: '[data-absmartly-hide]' // Custom selector for elements mode
|
|
1090
|
+
})
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
## Browser Compatibility
|
|
1094
|
+
|
|
1095
|
+
- Modern browsers (Chrome, Firefox, Safari, Edge)
|
|
1096
|
+
- Internet Explorer 11+ (with polyfills)
|
|
1097
|
+
- Mobile browsers (iOS Safari, Chrome Mobile)
|
|
1098
|
+
|
|
1099
|
+
## License
|
|
1100
|
+
|
|
1101
|
+
MIT
|