@datalyr/web 1.7.5 → 1.7.8
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 +542 -476
- package/dist/attribution-lyr.test.d.ts +2 -0
- package/dist/attribution-lyr.test.d.ts.map +1 -0
- package/dist/attribution-oppref.test.d.ts +2 -0
- package/dist/attribution-oppref.test.d.ts.map +1 -0
- package/dist/attribution.d.ts +4 -0
- package/dist/attribution.d.ts.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/datalyr.cjs.js +1303 -281
- package/dist/datalyr.cjs.js.map +1 -1
- package/dist/datalyr.esm.js +1303 -282
- package/dist/datalyr.esm.js.map +1 -1
- package/dist/datalyr.esm.min.js +1 -1
- package/dist/datalyr.esm.min.js.map +1 -1
- package/dist/datalyr.js +1303 -281
- package/dist/datalyr.js.map +1 -1
- package/dist/datalyr.min.js +1 -1
- package/dist/datalyr.min.js.map +1 -1
- package/dist/identify-dedupe.test.d.ts +2 -0
- package/dist/identify-dedupe.test.d.ts.map +1 -0
- package/dist/identity-pii.test.d.ts +2 -0
- package/dist/identity-pii.test.d.ts.map +1 -0
- package/dist/identity.d.ts +52 -4
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.d.ts +116 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.test.d.ts +3 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/queue-403.test.d.ts +2 -0
- package/dist/queue-403.test.d.ts.map +1 -0
- package/dist/queue.d.ts +32 -4
- package/dist/queue.d.ts.map +1 -1
- package/dist/storage-migration.test.d.ts +2 -0
- package/dist/storage-migration.test.d.ts.map +1 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/stripe-session.d.ts +75 -0
- package/dist/stripe-session.d.ts.map +1 -0
- package/dist/stripe-session.test.d.ts +2 -0
- package/dist/stripe-session.test.d.ts.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +13 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.test.d.ts +2 -1
- package/dist/utils.test.d.ts.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
# @datalyr/web
|
|
2
2
|
|
|
3
|
-
Browser SDK for event tracking, user identity, and attribution. Version 1.7.
|
|
3
|
+
Browser SDK for event tracking, user identity, and attribution. Version 1.7.8.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Full reference: [Web SDK](https://docs.datalyr.com/sdk-reference/web).
|
|
6
|
+
|
|
7
|
+
## Table of contents
|
|
6
8
|
|
|
7
9
|
- [Installation](#installation)
|
|
8
|
-
- [Quick
|
|
9
|
-
- [
|
|
10
|
+
- [Quick start](#quick-start)
|
|
11
|
+
- [What the SDK tracks without your code](#what-the-sdk-tracks-without-your-code)
|
|
12
|
+
- [Events the SDK sends for you](#events-the-sdk-sends-for-you)
|
|
13
|
+
- [Identity](#identity)
|
|
10
14
|
- [Configuration](#configuration)
|
|
11
|
-
- [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- [Web-to-
|
|
23
|
-
- [SPA
|
|
24
|
-
- [Framework
|
|
15
|
+
- [Remote configuration](#remote-configuration)
|
|
16
|
+
- [Complete method list](#complete-method-list)
|
|
17
|
+
- [Event tracking](#event-tracking)
|
|
18
|
+
- [Attribution](#attribution)
|
|
19
|
+
- [Checkout metadata](#checkout-metadata)
|
|
20
|
+
- [Super properties](#super-properties)
|
|
21
|
+
- [Privacy and consent](#privacy-and-consent)
|
|
22
|
+
- [Queue, network, and limits](#queue-network-and-limits)
|
|
23
|
+
- [Storage](#storage)
|
|
24
|
+
- [Container scripts](#container-scripts)
|
|
25
|
+
- [Debugging](#debugging)
|
|
26
|
+
- [Web-to-app attribution](#web-to-app-attribution)
|
|
27
|
+
- [SPA support](#spa-support)
|
|
28
|
+
- [Framework integration](#framework-integration)
|
|
25
29
|
- [TypeScript](#typescript)
|
|
30
|
+
- [Verify the install](#verify-the-install)
|
|
26
31
|
- [Troubleshooting](#troubleshooting)
|
|
27
32
|
- [License](#license)
|
|
28
33
|
|
|
@@ -30,419 +35,457 @@ Browser SDK for event tracking, user identity, and attribution. Version 1.7.0.
|
|
|
30
35
|
|
|
31
36
|
## Installation
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
Choose one method.
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```
|
|
40
|
+
> **Never load the script tag and the npm package together.** Two instances issue two
|
|
41
|
+
> `visitor_id` values for one person and split the journey.
|
|
38
42
|
|
|
39
|
-
### Script
|
|
43
|
+
### Script tag
|
|
40
44
|
|
|
41
45
|
```html
|
|
42
46
|
<script defer src="https://track.datalyr.com/dl.js"
|
|
43
47
|
data-workspace-id="YOUR_WORKSPACE_ID"></script>
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
The script tag loads externally
|
|
50
|
+
The script tag loads externally and initializes on its own. Reach the SDK at
|
|
51
|
+
`window.datalyr`.
|
|
52
|
+
|
|
53
|
+
The script tag reads these attributes:
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
| Attribute | Required | Effect |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `data-workspace-id` | Yes | The workspace that receives events. |
|
|
58
|
+
| `data-endpoint` | No | Overrides the ingest URL. |
|
|
59
|
+
| `data-tracked-params` | No | More URL parameters to capture. Comma-separated. |
|
|
60
|
+
| `data-debug` | No | `true` turns on console logging. |
|
|
61
|
+
| `data-auto-identify` | No | `true` or `false`. |
|
|
62
|
+
| `data-platform` | No | `shopify`, `checkoutchamp`, or `generic`. |
|
|
63
|
+
| `data-checkout-champ-domains` | No | Comma-separated domains. |
|
|
64
|
+
| `data-stripe-payment-links` | No | `false` turns off Stripe link decoration. |
|
|
65
|
+
| `data-stripe-link-domains` | No | Comma-separated extra Stripe domains. |
|
|
66
|
+
|
|
67
|
+
### npm
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install @datalyr/web
|
|
71
|
+
```
|
|
49
72
|
|
|
50
|
-
|
|
|
51
|
-
|
|
52
|
-
| Bundle
|
|
53
|
-
| TypeScript | No | Yes |
|
|
54
|
-
| Access | `window.datalyr` | `import datalyr` |
|
|
73
|
+
| Property | Script tag | npm |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| Bundle cost | 0, loaded externally | 88 KB minified, 24 KB gzipped |
|
|
76
|
+
| TypeScript types | No | Yes |
|
|
77
|
+
| Access | `window.datalyr` | `import datalyr from '@datalyr/web'` |
|
|
55
78
|
| Initialize | Automatic | `datalyr.init()` |
|
|
56
79
|
|
|
57
80
|
---
|
|
58
81
|
|
|
59
|
-
## Quick
|
|
82
|
+
## Quick start
|
|
60
83
|
|
|
61
84
|
```javascript
|
|
62
85
|
import datalyr from '@datalyr/web';
|
|
63
86
|
|
|
64
|
-
datalyr.init({
|
|
65
|
-
workspaceId: 'YOUR_WORKSPACE_ID'
|
|
66
|
-
});
|
|
87
|
+
datalyr.init({ workspaceId: 'YOUR_WORKSPACE_ID' });
|
|
67
88
|
|
|
68
|
-
// Wait for
|
|
89
|
+
// Wait for encryption keys, container scripts, and the first page view
|
|
69
90
|
await datalyr.ready();
|
|
70
91
|
|
|
71
|
-
// Track events
|
|
72
92
|
datalyr.track('button_clicked', { button: 'signup' });
|
|
73
93
|
|
|
74
|
-
// Identify users
|
|
75
94
|
datalyr.identify('user_123', { email: 'user@example.com' });
|
|
76
95
|
|
|
77
|
-
// Track page views (page name goes in properties)
|
|
78
96
|
datalyr.page({ title: 'Pricing', variant: 'A' });
|
|
79
97
|
```
|
|
80
98
|
|
|
99
|
+
`init()` throws `Error('[Datalyr] workspaceId is required')` when `workspaceId` is
|
|
100
|
+
missing. A second `init()` call logs a warning and returns. Any tracking method called
|
|
101
|
+
before `init()` logs a warning and returns.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## What the SDK tracks without your code
|
|
106
|
+
|
|
107
|
+
Two triggers fire on their own.
|
|
108
|
+
|
|
109
|
+
| Trigger | Event name | Turn it off with |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| First page view after load | `pageview` | `trackPageViews: false` |
|
|
112
|
+
| SPA route change — `pushState`, `replaceState`, `popstate`, `hashchange` | `pageview` | `trackSPA: false` |
|
|
113
|
+
|
|
114
|
+
Nothing else is automatic. The SDK records no clicks, scrolls, form submits, outbound
|
|
115
|
+
links, or errors. There is no session-start event. Session data rides on every event.
|
|
116
|
+
Call `track()` for everything else.
|
|
117
|
+
|
|
81
118
|
---
|
|
82
119
|
|
|
83
|
-
##
|
|
120
|
+
## Events the SDK sends for you
|
|
84
121
|
|
|
85
|
-
|
|
86
|
-
2. Each event includes device context, session data, and attribution parameters.
|
|
87
|
-
3. Events are queued locally and sent in batches.
|
|
88
|
-
4. If the browser is offline, events are stored and sent when connectivity returns.
|
|
89
|
-
5. Events are processed server-side for analytics and attribution reporting.
|
|
122
|
+
These names are reserved. Never send one through `track()` yourself.
|
|
90
123
|
|
|
91
|
-
|
|
124
|
+
| Method you call | Event name sent |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `identify()` | `$identify` |
|
|
127
|
+
| `group()` | `$group` |
|
|
128
|
+
| `alias()` | `$alias` |
|
|
129
|
+
| `screen()` | `pageview`, with a `screen` property |
|
|
130
|
+
| `trackAppDownloadClick()` | `$app_download_click` |
|
|
131
|
+
| Auto-identify email capture | `$auto_identify`, then `$identify` |
|
|
92
132
|
|
|
93
|
-
|
|
133
|
+
### Event payload
|
|
94
134
|
|
|
95
135
|
```javascript
|
|
96
136
|
{
|
|
97
137
|
event_name: 'purchase',
|
|
98
|
-
event_data: {
|
|
138
|
+
event_data: { /* your properties, plus attribution, session, and context fields */ },
|
|
99
139
|
|
|
100
140
|
// Identity
|
|
101
|
-
|
|
102
|
-
distinct_id: 'user_123',
|
|
103
|
-
user_id: 'user_123',
|
|
104
|
-
session_id: '
|
|
141
|
+
visitor_id: 'anon_3d5cf66d-203f-4009-8bb0-f3714da152a4',
|
|
142
|
+
distinct_id: 'user_123',
|
|
143
|
+
user_id: 'user_123',
|
|
144
|
+
session_id: 'sess_9f2c1b40-1c2e-4d55-9a0b-6a4f7f1d2e33',
|
|
105
145
|
|
|
106
|
-
// Context
|
|
146
|
+
// Context, inside event_data
|
|
107
147
|
url: 'https://example.com/pricing',
|
|
108
148
|
title: 'Pricing',
|
|
109
149
|
referrer: 'https://google.com',
|
|
110
150
|
|
|
111
|
-
// Attribution
|
|
151
|
+
// Attribution, inside event_data
|
|
112
152
|
utm_source: 'facebook',
|
|
113
153
|
fbclid: 'abc123',
|
|
114
154
|
|
|
115
155
|
// Metadata
|
|
116
156
|
workspace_id: 'wk_xxxxx',
|
|
117
157
|
source: 'web',
|
|
118
|
-
timestamp: '
|
|
119
|
-
sdk_version: '1.7.
|
|
158
|
+
timestamp: '2026-07-25T10:30:00Z',
|
|
159
|
+
sdk_version: '1.7.8',
|
|
120
160
|
sdk_name: 'datalyr-web-sdk'
|
|
121
161
|
}
|
|
122
162
|
```
|
|
123
163
|
|
|
124
164
|
---
|
|
125
165
|
|
|
126
|
-
##
|
|
127
|
-
|
|
128
|
-
All properties of `DatalyrConfig`:
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
datalyr.init({
|
|
132
|
-
// Required
|
|
133
|
-
workspaceId: string,
|
|
134
|
-
|
|
135
|
-
// Endpoint
|
|
136
|
-
endpoint?: string, // Default: 'https://ingest.datalyr.com'
|
|
137
|
-
fallbackEndpoints?: string[], // Default: [] - Additional endpoints to try on failure
|
|
138
|
-
|
|
139
|
-
// Debugging
|
|
140
|
-
debug?: boolean, // Default: false - Enable console logging
|
|
141
|
-
|
|
142
|
-
// Batching
|
|
143
|
-
batchSize?: number, // Default: 10 - Events per batch
|
|
144
|
-
flushInterval?: number, // Default: 5000 - Flush interval in ms
|
|
145
|
-
flushAt?: number, // Default: 10 - Flush when N events queued
|
|
146
|
-
|
|
147
|
-
// Priority events (bypass normal batching)
|
|
148
|
-
criticalEvents?: string[], // Default: undefined (disabled). Events flushed immediately.
|
|
149
|
-
highPriorityEvents?: string[], // Default: undefined (disabled). Events given queue priority.
|
|
150
|
-
|
|
151
|
-
// Session
|
|
152
|
-
sessionTimeout?: number, // Default: 3600000 (60 min)
|
|
153
|
-
trackSessions?: boolean, // Default: true
|
|
154
|
-
|
|
155
|
-
// Attribution
|
|
156
|
-
attributionWindow?: number, // Default: 7776000000 (90 days)
|
|
157
|
-
trackedParams?: string[], // Default: [] - Additional URL params to capture
|
|
158
|
-
|
|
159
|
-
// Privacy
|
|
160
|
-
respectDoNotTrack?: boolean, // Default: false - Honor browser DNT header
|
|
161
|
-
respectGlobalPrivacyControl?: boolean, // Default: true - Honor GPC signal
|
|
162
|
-
privacyMode?: 'standard' | 'strict', // Default: 'standard' - 'strict' limits data collection
|
|
163
|
-
|
|
164
|
-
// Cookies / Storage
|
|
165
|
-
cookieDomain?: string | 'auto', // Default: 'auto'
|
|
166
|
-
cookieExpires?: number, // Default: 365 (days)
|
|
167
|
-
secureCookie?: boolean | 'auto', // Default: 'auto'
|
|
168
|
-
sameSite?: 'Strict' | 'Lax' | 'None', // Default: 'Lax'
|
|
169
|
-
cookiePrefix?: string, // Default: '__dl_'
|
|
170
|
-
|
|
171
|
-
// Performance
|
|
172
|
-
enablePerformanceTracking?: boolean, // Default: true - Collect navigation timing metrics
|
|
173
|
-
enableFingerprinting?: boolean, // Default: true - Minimal device fingerprinting (standard mode only)
|
|
174
|
-
|
|
175
|
-
// Retry / Offline
|
|
176
|
-
maxRetries?: number, // Default: 5
|
|
177
|
-
retryDelay?: number, // Default: 1000 (ms)
|
|
178
|
-
maxOfflineQueueSize?: number, // Default: 100
|
|
179
|
-
|
|
180
|
-
// SPA
|
|
181
|
-
trackSPA?: boolean, // Default: true - Auto-track route changes
|
|
182
|
-
trackPageViews?: boolean, // Default: true - Auto-track initial page view
|
|
183
|
-
|
|
184
|
-
// Container scripts
|
|
185
|
-
enableContainer?: boolean, // Default: true - Load third-party scripts from dashboard
|
|
186
|
-
|
|
187
|
-
// Auto-Identify (opt-in)
|
|
188
|
-
autoIdentify?: boolean, // Default: false - Automatically identify users
|
|
189
|
-
autoIdentifyForms?: boolean, // Default: true - Capture email from forms (when autoIdentify enabled)
|
|
190
|
-
autoIdentifyAPI?: boolean, // Default: false - Capture email from API responses (off: same-origin scan can mis-identify; opt in explicitly)
|
|
191
|
-
autoIdentifyShopify?: boolean, // Default: true - Capture email from Shopify endpoints (when autoIdentify enabled)
|
|
192
|
-
autoIdentifyTrustedDomains?: string[], // Default: [] - Additional domains to trust for API capture
|
|
193
|
-
|
|
194
|
-
// Plugins
|
|
195
|
-
plugins?: DatalyrPlugin[], // Default: [] - Custom plugin instances
|
|
196
|
-
});
|
|
197
|
-
```
|
|
166
|
+
## Identity
|
|
198
167
|
|
|
199
|
-
|
|
168
|
+
Four identifiers ship on every event. They are not alternatives to each other.
|
|
200
169
|
|
|
201
|
-
|
|
170
|
+
| Wire field | Value |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `visitor_id` | The anonymous browser ID, format `anon_<uuid>`. Stays anonymous even after `identify()`. |
|
|
173
|
+
| `user_id` | The ID you pass to `identify()`. `null` until then. |
|
|
174
|
+
| `distinct_id` | The `user_id` when identified. The `visitor_id` before that. |
|
|
175
|
+
| `session_id` | Format `sess_<uuid>`. New after 60 minutes idle. |
|
|
202
176
|
|
|
203
|
-
|
|
177
|
+
Use `visitor_id` in server-side and webhook work. Stripe, Whop, and Shopify all key off
|
|
178
|
+
it. Use `distinct_id` when you reason about event-level identity.
|
|
204
179
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
- `checkoutChampDomains`
|
|
208
|
-
- `respectGlobalPrivacyControl`, `respectDoNotTrack`, `privacyMode`
|
|
180
|
+
The **Events** and **Conversions** tables label this column `distinct id`. For an
|
|
181
|
+
unidentified visitor it shows the `visitor_id`.
|
|
209
182
|
|
|
210
|
-
|
|
183
|
+
### Identity methods
|
|
211
184
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
185
|
+
| Method | Returns |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `getVisitorId(): string` | The anonymous browser ID. Same value as `getAnonymousId()`. |
|
|
188
|
+
| `getAnonymousId(): string` | The anonymous browser ID. |
|
|
189
|
+
| `getUserId(): string \| null` | The identified user ID, or `null`. |
|
|
190
|
+
| `getDistinctId(): string` | The user ID when identified, otherwise the anonymous browser ID. |
|
|
191
|
+
| `getSessionId(): string` | The current session ID. |
|
|
192
|
+
| `getSessionData(): SessionData \| null` | The full session record. |
|
|
193
|
+
| `startNewSession(): string` | Ends the current session and returns a new session ID. |
|
|
194
|
+
| `getWorkspaceId(): string \| null` | The workspace ID passed to `init()`. |
|
|
215
195
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
196
|
+
```typescript
|
|
197
|
+
interface SessionData {
|
|
198
|
+
id: string;
|
|
199
|
+
startTime: number;
|
|
200
|
+
lastActivity: number;
|
|
201
|
+
pageViews: number;
|
|
202
|
+
events: number;
|
|
203
|
+
duration: number;
|
|
204
|
+
isActive: boolean;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
221
207
|
|
|
222
|
-
|
|
208
|
+
### identify()
|
|
223
209
|
|
|
224
210
|
```typescript
|
|
225
|
-
|
|
211
|
+
identify(userId: string, traits?: UserTraits): void
|
|
226
212
|
```
|
|
227
213
|
|
|
228
214
|
```javascript
|
|
229
|
-
datalyr.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
product_name: 'Blue Shirt',
|
|
234
|
-
price: 29.99,
|
|
235
|
-
currency: 'USD',
|
|
236
|
-
category: 'Apparel',
|
|
215
|
+
datalyr.identify('user_123', {
|
|
216
|
+
email: 'user@example.com',
|
|
217
|
+
name: 'John Doe',
|
|
218
|
+
plan: 'premium',
|
|
237
219
|
});
|
|
238
220
|
```
|
|
239
221
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
222
|
+
Call `identify()` after sign-in. Traits are encrypted with AES-GCM before storage.
|
|
223
|
+
Passing a different `user_id` runs `reset()` first, so two people on one browser do not
|
|
224
|
+
merge. `identify()` does not rotate the session ID.
|
|
243
225
|
|
|
244
|
-
|
|
245
|
-
page(properties?: PageProperties): void
|
|
246
|
-
```
|
|
226
|
+
### reset()
|
|
247
227
|
|
|
248
228
|
```javascript
|
|
249
|
-
|
|
250
|
-
datalyr.page();
|
|
251
|
-
|
|
252
|
-
// Override title, add custom properties
|
|
253
|
-
datalyr.page({ title: 'Pricing', variant: 'A' });
|
|
254
|
-
|
|
255
|
-
// Add custom properties
|
|
256
|
-
datalyr.page({ product_id: 'SKU123', source: 'search' });
|
|
229
|
+
datalyr.reset();
|
|
257
230
|
```
|
|
258
231
|
|
|
259
|
-
|
|
232
|
+
> **Do not call `reset()` on every page load.** It issues a **new** `visitor_id`, which
|
|
233
|
+
> breaks the link between the campaign visit and the later conversion.
|
|
260
234
|
|
|
261
|
-
|
|
235
|
+
Call `reset()` on logout. It clears `user_id`, traits, super properties, the captured
|
|
236
|
+
email, the journey, and both touchpoints, then issues a new `visitor_id` and a new
|
|
237
|
+
session. It leaves the Meta `_fbc` and `_fbp` cookies in place.
|
|
262
238
|
|
|
263
|
-
|
|
239
|
+
### alias()
|
|
264
240
|
|
|
265
241
|
```typescript
|
|
266
|
-
|
|
242
|
+
alias(userId: string, previousId?: string): void
|
|
267
243
|
```
|
|
268
244
|
|
|
269
245
|
```javascript
|
|
270
|
-
datalyr.
|
|
271
|
-
datalyr.screen('Product Details', { product_id: 'SKU123' });
|
|
246
|
+
datalyr.alias('user_123');
|
|
272
247
|
```
|
|
273
248
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
249
|
+
> **`previousId` accepts only the current anonymous ID.** Any other value logs
|
|
250
|
+
> `alias() only accepts the current anonymous ID as previousId` and does nothing. Omit the
|
|
251
|
+
> argument to use the current anonymous ID.
|
|
277
252
|
|
|
278
|
-
|
|
253
|
+
### group()
|
|
279
254
|
|
|
280
255
|
```typescript
|
|
281
|
-
|
|
282
|
-
targetPlatform: 'ios' | 'android';
|
|
283
|
-
appStoreUrl: string;
|
|
284
|
-
}): void
|
|
256
|
+
group(groupId: string, traits?: Record<string, any>): void
|
|
285
257
|
```
|
|
286
258
|
|
|
287
259
|
```javascript
|
|
288
|
-
datalyr.
|
|
289
|
-
targetPlatform: 'ios',
|
|
290
|
-
appStoreUrl: 'https://apps.apple.com/app/your-app/id123456789',
|
|
291
|
-
});
|
|
260
|
+
datalyr.group('company_abc', { name: 'Acme Inc', plan: 'enterprise' });
|
|
292
261
|
```
|
|
293
262
|
|
|
294
|
-
For Android Play Store URLs, attribution parameters are automatically encoded into the `referrer` query parameter for deterministic install attribution.
|
|
295
|
-
|
|
296
263
|
---
|
|
297
264
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
#### `identify(userId, traits?)`
|
|
265
|
+
## Configuration
|
|
301
266
|
|
|
302
|
-
|
|
267
|
+
Only `workspaceId` is required.
|
|
303
268
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
269
|
+
> **`sessionTimeout` and `attributionWindow` are milliseconds, not minutes or days.**
|
|
270
|
+
> Setting `sessionTimeout: 60` expires the session after 60 milliseconds and starts a new
|
|
271
|
+
> session on every event.
|
|
307
272
|
|
|
308
273
|
```javascript
|
|
309
|
-
datalyr.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
274
|
+
datalyr.init({
|
|
275
|
+
workspaceId: 'YOUR_WORKSPACE_ID',
|
|
276
|
+
sessionTimeout: 3600000, // 60 minutes, in milliseconds
|
|
277
|
+
attributionWindow: 7776000000, // 90 days, in milliseconds
|
|
278
|
+
debug: true,
|
|
314
279
|
});
|
|
315
280
|
```
|
|
316
281
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
282
|
+
| Option | Type | Default |
|
|
283
|
+
| --- | --- | --- |
|
|
284
|
+
| `workspaceId` | `string` | Required. Throws when missing. |
|
|
285
|
+
| `endpoint` | `string` | `'https://ingest.datalyr.com'` |
|
|
286
|
+
| `fallbackEndpoints` | `string[]` | `[]`. Tried in order after the primary endpoint fails. |
|
|
287
|
+
| `debug` | `boolean` | `false` |
|
|
288
|
+
| `batchSize` | `number` events | `10`, clamped 1–1000 |
|
|
289
|
+
| `flushInterval` | `number` ms | `5000`, clamped 250–3600000 |
|
|
290
|
+
| `criticalEvents` | `string[]` | `['purchase', 'signup', 'subscribe', 'lead', 'conversion']`. Flushed immediately. |
|
|
291
|
+
| `highPriorityEvents` | `string[]` | `['add_to_cart', 'begin_checkout', 'view_item', 'search']`. Flushed after 1000 ms. |
|
|
292
|
+
| `sessionTimeout` | `number` ms | `3600000` — 60 minutes |
|
|
293
|
+
| `attributionWindow` | `number` ms | `7776000000` — 90 days |
|
|
294
|
+
| `trackedParams` | `string[]` | `[]`, merged onto the default list |
|
|
295
|
+
| `respectDoNotTrack` | `boolean` | `false` |
|
|
296
|
+
| `respectGlobalPrivacyControl` | `boolean` | `true` |
|
|
297
|
+
| `privacyMode` | `'standard' \| 'strict'` | `'standard'` |
|
|
298
|
+
| `cookieDomain` | `string \| 'auto'` | `'auto'` |
|
|
299
|
+
| `cookieExpires` | `number` days | `365` |
|
|
300
|
+
| `secureCookie` | `boolean \| 'auto'` | `'auto'` |
|
|
301
|
+
| `sameSite` | `'Strict' \| 'Lax' \| 'None'` | `'Lax'` |
|
|
302
|
+
| `cookiePrefix` | `string` | `'__dl_'` |
|
|
303
|
+
| `enablePerformanceTracking` | `boolean` | `true` |
|
|
304
|
+
| `enableFingerprinting` | `boolean` | `true`. Off in `strict` privacy mode. |
|
|
305
|
+
| `maxRetries` | `number` | `5`, clamped 0–20 |
|
|
306
|
+
| `retryDelay` | `number` ms | `1000`, clamped 0–60000 |
|
|
307
|
+
| `maxOfflineQueueSize` | `number` events | `100`, clamped 1–100000 |
|
|
308
|
+
| `trackSPA` | `boolean` | `true` |
|
|
309
|
+
| `trackPageViews` | `boolean` | `true` |
|
|
310
|
+
| `enableContainer` | `boolean` | `true`. Set `false` to skip dashboard-configured pixels. |
|
|
311
|
+
| `autoIdentify` | `boolean` | `false`. Forced `false` when `privacyMode` is `'strict'`. |
|
|
312
|
+
| `autoIdentifyForms` | `boolean` | `true`, applied only when `autoIdentify` is `true` |
|
|
313
|
+
| `autoIdentifyAPI` | `boolean` | `false`. A same-origin response scan can mis-identify. |
|
|
314
|
+
| `autoIdentifyShopify` | `boolean` | `true`, applied only when `autoIdentify` is `true` |
|
|
315
|
+
| `autoIdentifyTrustedDomains` | `string[]` | `[]` |
|
|
316
|
+
| `shopifyCartAttributes` | `boolean` | `false`. Becomes `true` when `platform` is `'shopify'`. |
|
|
317
|
+
| `platform` | `'shopify' \| 'checkoutchamp' \| 'generic'` | Unset |
|
|
318
|
+
| `checkoutChampDomains` | `string[]` | Unset |
|
|
319
|
+
| `stripePaymentLinks` | `boolean` | `true` |
|
|
320
|
+
| `stripeLinkDomains` | `string[]` | `[]` |
|
|
321
|
+
| `plugins` | `DatalyrPlugin[]` | `[]` |
|
|
322
|
+
|
|
323
|
+
### Options that do nothing
|
|
324
|
+
|
|
325
|
+
`DatalyrConfig` declares two options that no code reads. Setting either changes no
|
|
326
|
+
behavior.
|
|
327
|
+
|
|
328
|
+
| Option | Status |
|
|
329
|
+
| --- | --- |
|
|
330
|
+
| `flushAt` | Declared and given a default of `10`. Never read. Use `batchSize`. |
|
|
331
|
+
| `trackSessions` | Declared and given a default of `true`. Never read. Sessions are always tracked. |
|
|
328
332
|
|
|
329
|
-
|
|
333
|
+
---
|
|
330
334
|
|
|
331
|
-
|
|
335
|
+
## Remote configuration
|
|
332
336
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
337
|
+
Datalyr can set these nine options from the dashboard, under **Settings → Identity &
|
|
338
|
+
Attribution**, with no code change. The SDK receives them at runtime in the
|
|
339
|
+
`/container-scripts` response.
|
|
336
340
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
341
|
+
`autoIdentify`, `autoIdentifyForms`, `autoIdentifyAPI`, `autoIdentifyShopify`,
|
|
342
|
+
`shopifyCartAttributes`, `checkoutChampDomains`, `respectGlobalPrivacyControl`,
|
|
343
|
+
`respectDoNotTrack`, `privacyMode`.
|
|
340
344
|
|
|
341
|
-
|
|
345
|
+
Precedence per key: built-in default, then the dashboard value, then the explicit
|
|
346
|
+
`init()` value. A value you pass to `init()` always wins. Changes propagate within about
|
|
347
|
+
5 minutes because the response is edge-cached.
|
|
342
348
|
|
|
343
|
-
|
|
349
|
+
`platform` is not remote-configurable. Set it at install time with the `data-platform`
|
|
350
|
+
attribute. The CheckoutChamp behaviors run before the remote config arrives.
|
|
344
351
|
|
|
345
|
-
|
|
346
|
-
datalyr.reset();
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
#### `getAnonymousId()`
|
|
352
|
+
---
|
|
350
353
|
|
|
351
|
-
|
|
354
|
+
## Complete method list
|
|
355
|
+
|
|
356
|
+
| Method | Signature |
|
|
357
|
+
| --- | --- |
|
|
358
|
+
| `init` | `(config: DatalyrConfig) => void` |
|
|
359
|
+
| `ready` | `() => Promise<void>` |
|
|
360
|
+
| `track` | `(eventName: string, properties?: EventProperties) => void` |
|
|
361
|
+
| `identify` | `(userId: string, traits?: UserTraits) => void` |
|
|
362
|
+
| `page` | `(properties?: PageProperties) => void` |
|
|
363
|
+
| `screen` | `(screenName: string, properties?: Record<string, any>) => void` |
|
|
364
|
+
| `group` | `(groupId: string, traits?: Record<string, any>) => void` |
|
|
365
|
+
| `alias` | `(userId: string, previousId?: string) => void` |
|
|
366
|
+
| `reset` | `() => void` |
|
|
367
|
+
| `flush` | `() => Promise<void>` |
|
|
368
|
+
| `destroy` | `() => void` |
|
|
369
|
+
| `getWorkspaceId` | `() => string \| null` |
|
|
370
|
+
| `getVisitorId` | `() => string` |
|
|
371
|
+
| `getAnonymousId` | `() => string` |
|
|
372
|
+
| `getUserId` | `() => string \| null` |
|
|
373
|
+
| `getDistinctId` | `() => string` |
|
|
374
|
+
| `getSessionId` | `() => string` |
|
|
375
|
+
| `getSessionData` | `() => SessionData \| null` |
|
|
376
|
+
| `startNewSession` | `() => string` |
|
|
377
|
+
| `getAttribution` | `() => Attribution` |
|
|
378
|
+
| `getJourney` | `() => TouchPoint[]` |
|
|
379
|
+
| `setAttribution` | `(attribution: Partial<Attribution>) => void` |
|
|
380
|
+
| `getStripeMetadata` | `() => { client_reference_id: string; metadata: { visitor_id: string } }` |
|
|
381
|
+
| `getWhopCheckoutMetadata` | `() => { visitor_id: string }` |
|
|
382
|
+
| `trackAppDownloadClick` | `(options: { targetPlatform: 'ios' \| 'android'; appStoreUrl: string }) => Promise<void>` |
|
|
383
|
+
| `setConsent` | `(consent: ConsentConfig) => void` |
|
|
384
|
+
| `optOut` | `() => void` |
|
|
385
|
+
| `optIn` | `() => void` |
|
|
386
|
+
| `isOptedOut` | `() => boolean` |
|
|
387
|
+
| `setSuperProperties` | `(properties: Record<string, any>) => void` |
|
|
388
|
+
| `unsetSuperProperty` | `(propertyName: string) => void` |
|
|
389
|
+
| `getSuperProperties` | `() => Record<string, any>` |
|
|
390
|
+
| `getErrors` | `() => ErrorInfo[]` |
|
|
391
|
+
| `getNetworkStatus` | `() => NetworkStatus` |
|
|
392
|
+
| `loadScript` | `(scriptId: string) => void` |
|
|
393
|
+
| `getLoadedScripts` | `() => string[]` |
|
|
394
|
+
|
|
395
|
+
The default export and `window.datalyr` are one shared singleton.
|
|
396
|
+
`createDatalyrInstance()` returns a second independent instance.
|
|
397
|
+
|
|
398
|
+
`trackImmediate()` and `forceFlush()` are not public methods. Names in the bootstrap stub
|
|
399
|
+
that resemble them do not resolve to anything callable. Use `track()` and `flush()`.
|
|
352
400
|
|
|
353
|
-
|
|
354
|
-
getAnonymousId(): string
|
|
355
|
-
```
|
|
401
|
+
---
|
|
356
402
|
|
|
357
|
-
|
|
403
|
+
## Event tracking
|
|
358
404
|
|
|
359
|
-
|
|
405
|
+
### track()
|
|
360
406
|
|
|
361
407
|
```typescript
|
|
362
|
-
|
|
408
|
+
track(eventName: string, properties?: EventProperties): void
|
|
363
409
|
```
|
|
364
410
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
Returns the distinct ID. This is `userId` if identified, otherwise `anonymousId`.
|
|
411
|
+
```javascript
|
|
412
|
+
datalyr.track('signup_started');
|
|
368
413
|
|
|
369
|
-
|
|
370
|
-
|
|
414
|
+
datalyr.track('product_viewed', {
|
|
415
|
+
product_id: 'SKU123',
|
|
416
|
+
product_name: 'Blue Shirt',
|
|
417
|
+
price: 29.99,
|
|
418
|
+
currency: 'USD',
|
|
419
|
+
category: 'Apparel',
|
|
420
|
+
});
|
|
371
421
|
```
|
|
372
422
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
### Session
|
|
376
|
-
|
|
377
|
-
#### `getSessionId()`
|
|
378
|
-
|
|
379
|
-
Returns the current session ID.
|
|
423
|
+
### page()
|
|
380
424
|
|
|
381
425
|
```typescript
|
|
382
|
-
|
|
426
|
+
page(properties?: PageProperties): void
|
|
383
427
|
```
|
|
384
428
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
Force-start a new session. Returns the new session ID.
|
|
388
|
-
|
|
389
|
-
```typescript
|
|
390
|
-
startNewSession(): string
|
|
391
|
-
```
|
|
429
|
+
The SDK captures `title`, `url`, `path`, `search`, and `referrer` on its own. Pass
|
|
430
|
+
`properties` to add or override values.
|
|
392
431
|
|
|
393
432
|
```javascript
|
|
394
|
-
|
|
433
|
+
datalyr.page();
|
|
434
|
+
datalyr.page({ title: 'Pricing', variant: 'A' });
|
|
395
435
|
```
|
|
396
436
|
|
|
397
|
-
|
|
437
|
+
`page()` does not accept a name string as its first argument. Pass the page name as
|
|
438
|
+
`title` inside the properties object.
|
|
439
|
+
|
|
440
|
+
Use `page()` only when automatic tracking is off, or for a second view on one URL.
|
|
398
441
|
|
|
399
|
-
|
|
442
|
+
### screen()
|
|
400
443
|
|
|
401
444
|
```typescript
|
|
402
|
-
|
|
445
|
+
screen(screenName: string, properties?: Record<string, any>): void
|
|
403
446
|
```
|
|
404
447
|
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
startTime: number;
|
|
409
|
-
lastActivity: number;
|
|
410
|
-
pageViews: number;
|
|
411
|
-
events: number;
|
|
412
|
-
duration: number;
|
|
413
|
-
isActive: boolean;
|
|
414
|
-
}
|
|
448
|
+
```javascript
|
|
449
|
+
datalyr.screen('Dashboard');
|
|
450
|
+
datalyr.screen('Product Details', { product_id: 'SKU123' });
|
|
415
451
|
```
|
|
416
452
|
|
|
417
|
-
|
|
453
|
+
`screen()` sends a `pageview` event with the `screen` property set.
|
|
418
454
|
|
|
419
|
-
|
|
455
|
+
### ready()
|
|
420
456
|
|
|
421
457
|
```typescript
|
|
422
458
|
ready(): Promise<void>
|
|
423
459
|
```
|
|
424
460
|
|
|
461
|
+
`init()` returns synchronously. Encryption setup, container loading, and the first page
|
|
462
|
+
view finish asynchronously. Await `ready()` when you need all three complete.
|
|
463
|
+
|
|
425
464
|
```javascript
|
|
426
465
|
datalyr.init({ workspaceId: 'YOUR_WORKSPACE_ID' });
|
|
427
466
|
await datalyr.ready();
|
|
428
|
-
|
|
467
|
+
datalyr.track('post_init_event');
|
|
429
468
|
```
|
|
430
469
|
|
|
431
470
|
---
|
|
432
471
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
#### `getAttribution()`
|
|
436
|
-
|
|
437
|
-
Returns the current captured attribution data.
|
|
472
|
+
## Attribution
|
|
438
473
|
|
|
439
474
|
```typescript
|
|
440
475
|
getAttribution(): Attribution
|
|
476
|
+
setAttribution(attribution: Partial<Attribution>): void
|
|
477
|
+
getJourney(): TouchPoint[]
|
|
441
478
|
```
|
|
442
479
|
|
|
443
480
|
```javascript
|
|
444
481
|
const attribution = datalyr.getAttribution();
|
|
445
482
|
console.log(attribution.source, attribution.medium, attribution.campaign);
|
|
483
|
+
|
|
484
|
+
datalyr.setAttribution({
|
|
485
|
+
source: 'partner',
|
|
486
|
+
medium: 'referral',
|
|
487
|
+
campaign: 'spring_promo',
|
|
488
|
+
});
|
|
446
489
|
```
|
|
447
490
|
|
|
448
491
|
```typescript
|
|
@@ -453,51 +496,15 @@ interface Attribution {
|
|
|
453
496
|
term?: string | null;
|
|
454
497
|
content?: string | null;
|
|
455
498
|
clickId?: string | null;
|
|
456
|
-
clickIdType?: string | null; // 'fbclid', 'gclid', '
|
|
499
|
+
clickIdType?: string | null; // 'fbclid', 'gclid', 'ttclid', and the rest of the 15 below
|
|
457
500
|
referrer?: string | null;
|
|
458
501
|
referrerHost?: string | null;
|
|
459
502
|
landingPage?: string | null;
|
|
460
503
|
landingPath?: string | null;
|
|
461
504
|
timestamp?: number;
|
|
462
|
-
[key: string]: any; //
|
|
505
|
+
[key: string]: any; // Your trackedParams
|
|
463
506
|
}
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
Captured automatically from URLs:
|
|
467
|
-
|
|
468
|
-
| Type | Parameters |
|
|
469
|
-
|------|------------|
|
|
470
|
-
| UTM | `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term` |
|
|
471
|
-
| Click IDs | `fbclid`, `gclid`, `ttclid`, `oppref`, `twclid`, `li_fat_id`, `msclkid` |
|
|
472
|
-
| Referrer | `referrer`, `landing_page` |
|
|
473
|
-
|
|
474
|
-
Attribution supports first-touch (90-day window), last-touch, and journey tracking.
|
|
475
|
-
|
|
476
|
-
#### `setAttribution(attribution)`
|
|
477
|
-
|
|
478
|
-
Manually set or override attribution data.
|
|
479
|
-
|
|
480
|
-
```typescript
|
|
481
|
-
setAttribution(attribution: Partial<Attribution>): void
|
|
482
|
-
```
|
|
483
507
|
|
|
484
|
-
```javascript
|
|
485
|
-
datalyr.setAttribution({
|
|
486
|
-
source: 'partner',
|
|
487
|
-
medium: 'referral',
|
|
488
|
-
campaign: 'spring_promo',
|
|
489
|
-
});
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
#### `getJourney()`
|
|
493
|
-
|
|
494
|
-
Returns the customer journey as an array of touchpoints (up to 30).
|
|
495
|
-
|
|
496
|
-
```typescript
|
|
497
|
-
getJourney(): TouchPoint[]
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
```typescript
|
|
501
508
|
interface TouchPoint {
|
|
502
509
|
timestamp: number;
|
|
503
510
|
source?: string;
|
|
@@ -507,75 +514,130 @@ interface TouchPoint {
|
|
|
507
514
|
}
|
|
508
515
|
```
|
|
509
516
|
|
|
510
|
-
|
|
517
|
+
First touch and last touch persist for `attributionWindow`, 90 days by default. The
|
|
518
|
+
journey keeps the last 30 touchpoints.
|
|
511
519
|
|
|
512
|
-
###
|
|
520
|
+
### Click IDs captured
|
|
513
521
|
|
|
514
|
-
|
|
522
|
+
The SDK reads these 16 parameters from the URL. Matching is **case-sensitive**.
|
|
523
|
+
They are listed in precedence order, down the left column then down the right.
|
|
515
524
|
|
|
516
|
-
|
|
525
|
+
| Parameter | Platform | Parameter | Platform |
|
|
526
|
+
| --- | --- | --- | --- |
|
|
527
|
+
| `fbclid` | Meta | `li_fat_id` | LinkedIn |
|
|
528
|
+
| `gclid` | Google Ads | `sclid` | Snapchat |
|
|
529
|
+
| `gbraid` | Google Ads, iOS | `dclid` | Google Display |
|
|
530
|
+
| `wbraid` | Google Ads, web | `epik` | Pinterest |
|
|
531
|
+
| `ttclid` | TikTok | `rdt_cid` | Reddit |
|
|
532
|
+
| `oppref` | OpenAI Ads | `obclid` | Outbrain |
|
|
533
|
+
| `msclkid` | Microsoft | `irclid` | Impact |
|
|
534
|
+
| `twclid` | X | `ko_click_id` | Klaviyo |
|
|
517
535
|
|
|
518
|
-
|
|
536
|
+
Two parameters are normalized to a canonical name:
|
|
519
537
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
538
|
+
| In the URL | Stored as |
|
|
539
|
+
| --- | --- |
|
|
540
|
+
| `ScCid`, `sccid` | `sclid` |
|
|
541
|
+
| `irclickid` | `irclid` |
|
|
523
542
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
```
|
|
543
|
+
A URL carrying several click IDs sends all of them. `clickIdType` takes the first match in
|
|
544
|
+
the order of the table above. Any click ID forces `medium` to `cpc`.
|
|
527
545
|
|
|
528
|
-
|
|
546
|
+
`oppref` (OpenAI Ads) is captured **as of 1.7.8**. Earlier versions did not, so OpenAI Ads
|
|
547
|
+
web conversions from 1.7.7 and below delivered with `attribution_type: none` unless the
|
|
548
|
+
value was passed by hand. It is ordered after the Google click IDs, matching the iOS and
|
|
549
|
+
React Native SDKs, so it never outranks `fbclid` or `gclid` on a URL carrying both.
|
|
529
550
|
|
|
530
|
-
|
|
551
|
+
### Campaign parameters captured
|
|
531
552
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
553
|
+
| Group | Parameters |
|
|
554
|
+
| --- | --- |
|
|
555
|
+
| UTM | `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content` |
|
|
556
|
+
| Always captured | `lyr`, `ref`, `source`, `campaign`, `medium`, `gad_source` |
|
|
557
|
+
| Your `trackedParams` | Added to the rows above, never replacing them |
|
|
535
558
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
```
|
|
559
|
+
Each UTM is sent twice, under its full name and its stripped alias. `utm_source` also
|
|
560
|
+
arrives as `source`.
|
|
539
561
|
|
|
540
|
-
|
|
562
|
+
### Ad platform cookies read
|
|
541
563
|
|
|
542
|
-
|
|
564
|
+
| Platform | Cookies | Survives a marketing decline |
|
|
565
|
+
| --- | --- | --- |
|
|
566
|
+
| Meta | `_fbp`, `_fbc` | No |
|
|
567
|
+
| Google Ads | `_gcl_aw`, `_gcl_dc`, `_gcl_gb`, `_gcl_ha`, `_gac` | No |
|
|
568
|
+
| Google Analytics | `_ga`, `_gid` | Yes |
|
|
569
|
+
| TikTok | `_ttp`, `_ttc` | No |
|
|
570
|
+
| Snapchat | `_scid` | No |
|
|
543
571
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
572
|
+
Campaign parameters survive a marketing decline.
|
|
573
|
+
|
|
574
|
+
The SDK writes `_fbc` and `_fbp` itself when an `fbclid` arrives and Meta has not set
|
|
575
|
+
them. Both last 90 days.
|
|
547
576
|
|
|
548
577
|
---
|
|
549
578
|
|
|
550
|
-
|
|
579
|
+
## Checkout metadata
|
|
551
580
|
|
|
552
|
-
|
|
581
|
+
```javascript
|
|
582
|
+
const stripe = datalyr.getStripeMetadata();
|
|
583
|
+
// { client_reference_id: 'anon_...', metadata: { visitor_id: 'anon_...' } }
|
|
553
584
|
|
|
554
|
-
|
|
585
|
+
const whop = datalyr.getWhopCheckoutMetadata();
|
|
586
|
+
// { visitor_id: 'anon_...' }
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Pass `getStripeMetadata()` into the Stripe object your server creates.
|
|
555
590
|
|
|
556
591
|
```javascript
|
|
557
|
-
|
|
592
|
+
// Server receives this from the browser, then creates the PaymentIntent
|
|
593
|
+
stripe.paymentIntents.create({
|
|
594
|
+
amount: 4999,
|
|
595
|
+
currency: 'usd',
|
|
596
|
+
metadata: datalyrStripeMetadata.metadata,
|
|
597
|
+
});
|
|
558
598
|
```
|
|
559
599
|
|
|
560
|
-
|
|
600
|
+
The Datalyr Stripe webhook reads `client_reference_id` on a Checkout Session and
|
|
601
|
+
`metadata.visitor_id` on every other Stripe object.
|
|
561
602
|
|
|
562
|
-
|
|
603
|
+
### Automatic Stripe link decoration
|
|
563
604
|
|
|
564
|
-
|
|
565
|
-
datalyr.optIn();
|
|
566
|
-
```
|
|
605
|
+
With `stripePaymentLinks: true`, the default, the SDK stamps:
|
|
567
606
|
|
|
568
|
-
|
|
607
|
+
| Surface | Receives |
|
|
608
|
+
| --- | --- |
|
|
609
|
+
| `<a>` links to `buy.stripe.com`, plus any host in `stripeLinkDomains` | `client_reference_id`, and `prefilled_email` when an email is known |
|
|
610
|
+
| `<stripe-pricing-table>`, `<stripe-buy-button>` | The `client-reference-id` attribute only, never an email |
|
|
569
611
|
|
|
570
|
-
|
|
612
|
+
The SDK skips `prefilled_email` when `prefilled_email` or `locked_prefilled_email` is
|
|
613
|
+
already present, or when `privacyMode` is `'strict'`. It re-stamps on click and watches
|
|
614
|
+
the DOM for new links, debounced at 150 ms.
|
|
615
|
+
|
|
616
|
+
> **The SDK never decorates `checkout.stripe.com` URLs.** Stripe ignores these parameters
|
|
617
|
+
> on an already-created Checkout Session. The SDK also cannot reach links opened with
|
|
618
|
+
> `window.open()`, links inside an iframe, or links in shadow DOM. Call `getVisitorId()`
|
|
619
|
+
> and pass the value yourself in those cases.
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## Super properties
|
|
624
|
+
|
|
625
|
+
Super properties attach to every later event. `reset()` clears them.
|
|
571
626
|
|
|
572
627
|
```typescript
|
|
573
|
-
|
|
628
|
+
setSuperProperties(properties: Record<string, any>): void
|
|
629
|
+
unsetSuperProperty(propertyName: string): void
|
|
630
|
+
getSuperProperties(): Record<string, any>
|
|
574
631
|
```
|
|
575
632
|
|
|
576
|
-
|
|
633
|
+
```javascript
|
|
634
|
+
datalyr.setSuperProperties({ app_version: '2.1.0', environment: 'production' });
|
|
635
|
+
datalyr.unsetSuperProperty('environment');
|
|
636
|
+
```
|
|
577
637
|
|
|
578
|
-
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Privacy and consent
|
|
579
641
|
|
|
580
642
|
```typescript
|
|
581
643
|
setConsent(consent: ConsentConfig): void
|
|
@@ -586,7 +648,7 @@ datalyr.setConsent({
|
|
|
586
648
|
analytics: true,
|
|
587
649
|
marketing: false,
|
|
588
650
|
preferences: true,
|
|
589
|
-
sale: false // CCPA "Do Not Sell"
|
|
651
|
+
sale: false, // CCPA "Do Not Sell"
|
|
590
652
|
});
|
|
591
653
|
```
|
|
592
654
|
|
|
@@ -599,42 +661,36 @@ interface ConsentConfig {
|
|
|
599
661
|
}
|
|
600
662
|
```
|
|
601
663
|
|
|
602
|
-
|
|
664
|
+
`setConsent()` is safe to call before `init()`. The SDK persists the choice and applies it
|
|
665
|
+
at initialization.
|
|
603
666
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
667
|
+
| Method | Effect |
|
|
668
|
+
| --- | --- |
|
|
669
|
+
| `optOut()` | Stops all tracking. Clears the queue. Writes the `__dl_opt_out` cookie. Stops Stripe link decoration. |
|
|
670
|
+
| `optIn()` | Resumes tracking. |
|
|
671
|
+
| `isOptedOut(): boolean` | The current opt-out state. |
|
|
607
672
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
673
|
+
`privacyMode: 'strict'` forces `autoIdentify` off, turns off fingerprinting, and stops
|
|
674
|
+
email prefill on Stripe links. The SDK honors Global Privacy Control by default. It
|
|
675
|
+
ignores Do Not Track unless `respectDoNotTrack` is `true`.
|
|
611
676
|
|
|
612
|
-
|
|
677
|
+
```javascript
|
|
678
|
+
datalyr.init({ workspaceId: 'YOUR_WORKSPACE_ID', privacyMode: 'strict' });
|
|
679
|
+
```
|
|
613
680
|
|
|
614
681
|
---
|
|
615
682
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
#### `flush()`
|
|
619
|
-
|
|
620
|
-
Manually flush the event queue. Returns a promise that resolves when all queued events are sent.
|
|
683
|
+
## Queue, network, and limits
|
|
621
684
|
|
|
622
685
|
```typescript
|
|
623
686
|
flush(): Promise<void>
|
|
687
|
+
getNetworkStatus(): NetworkStatus
|
|
624
688
|
```
|
|
625
689
|
|
|
626
690
|
```javascript
|
|
627
691
|
await datalyr.flush();
|
|
628
692
|
```
|
|
629
693
|
|
|
630
|
-
#### `getNetworkStatus()`
|
|
631
|
-
|
|
632
|
-
Returns the current network status as tracked by the SDK.
|
|
633
|
-
|
|
634
|
-
```typescript
|
|
635
|
-
getNetworkStatus(): NetworkStatus
|
|
636
|
-
```
|
|
637
|
-
|
|
638
694
|
```typescript
|
|
639
695
|
interface NetworkStatus {
|
|
640
696
|
isOnline: boolean;
|
|
@@ -643,48 +699,75 @@ interface NetworkStatus {
|
|
|
643
699
|
}
|
|
644
700
|
```
|
|
645
701
|
|
|
646
|
-
|
|
702
|
+
The SDK stores events while the browser is offline and sends them when connectivity
|
|
703
|
+
returns.
|
|
647
704
|
|
|
648
|
-
|
|
705
|
+
| Limit | Value |
|
|
706
|
+
| --- | --- |
|
|
707
|
+
| Events per batch | 10, configurable to 1000 through `batchSize` |
|
|
708
|
+
| Flush interval | 5000 ms |
|
|
709
|
+
| High-priority flush | 1000 ms |
|
|
710
|
+
| Retries per batch | 5 |
|
|
711
|
+
| Offline queue | 100 events. The oldest is dropped first. |
|
|
712
|
+
| `keepalive` request body | 60000 bytes |
|
|
713
|
+
| `sendBeacon` chunk | 60000 bytes |
|
|
714
|
+
| Duplicate suppression | The last 1000 event hashes |
|
|
715
|
+
| Journey length | 30 touchpoints |
|
|
716
|
+
| Property nesting depth | 5 |
|
|
717
|
+
| Error buffer | 50 |
|
|
718
|
+
| Container script fetch | 3000 ms timeout |
|
|
649
719
|
|
|
650
|
-
|
|
720
|
+
There is no per-event size limit. Only the transport limits above apply.
|
|
651
721
|
|
|
652
|
-
|
|
722
|
+
### How failed requests behave
|
|
653
723
|
|
|
654
|
-
|
|
724
|
+
| Response | Behavior |
|
|
725
|
+
| --- | --- |
|
|
726
|
+
| `429` | The SDK waits for `Retry-After`, default 60 seconds. The batch moves to the offline queue. |
|
|
727
|
+
| `403` | The SDK backs off for 5 minutes and parks the batch in the offline queue. Origin configuration changes, so the batch is not discarded. |
|
|
728
|
+
| Other `4xx`, except `408` | Dropped permanently. Never retried. This is what a wrong `workspaceId` produces. |
|
|
729
|
+
| `5xx`, `408`, network failure | Retried with exponential backoff, up to `maxRetries`, then tried against each `fallbackEndpoints` host. |
|
|
655
730
|
|
|
656
|
-
|
|
731
|
+
---
|
|
657
732
|
|
|
658
|
-
|
|
659
|
-
loadScript(scriptId: string): void
|
|
660
|
-
```
|
|
733
|
+
## Storage
|
|
661
734
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
735
|
+
| Cookie | Value | Lifetime |
|
|
736
|
+
| --- | --- | --- |
|
|
737
|
+
| `__dl_visitor_id` | The `visitor_id` | 365 days |
|
|
738
|
+
| `__dl_opt_out` | `true` or `false` | `cookieExpires` |
|
|
739
|
+
|
|
740
|
+
The SDK writes `__dl_visitor_id` on the root domain with `SameSite=Lax`, and `Secure` on
|
|
741
|
+
HTTPS. A failed root-domain write falls back to a host-only cookie.
|
|
742
|
+
|
|
743
|
+
State lives in `localStorage` under a double `dl_dl_` prefix: `dl_dl_anonymous_id`,
|
|
744
|
+
`dl_dl_user_id`, `dl_dl_journey`, `dl_dl_first_touch`, `dl_dl_last_touch`,
|
|
745
|
+
`dl_dl_offline_queue`. Traits and the captured email are encrypted with AES-GCM.
|
|
746
|
+
|
|
747
|
+
---
|
|
665
748
|
|
|
666
|
-
|
|
749
|
+
## Container scripts
|
|
667
750
|
|
|
668
|
-
|
|
751
|
+
Container scripts are the third-party pixels you configure in the Datalyr dashboard. The
|
|
752
|
+
container is built into `dl.js` and into the npm package. It loads on init. There is no
|
|
753
|
+
separate container snippet to install.
|
|
669
754
|
|
|
670
755
|
```typescript
|
|
756
|
+
loadScript(scriptId: string): void
|
|
671
757
|
getLoadedScripts(): string[]
|
|
672
758
|
```
|
|
673
759
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
<script defer src="https://track.datalyr.com/dl.js"
|
|
678
|
-
data-workspace-id="YOUR_WORKSPACE_ID"></script>
|
|
760
|
+
```javascript
|
|
761
|
+
datalyr.loadScript('custom-script-id');
|
|
762
|
+
console.log(datalyr.getLoadedScripts());
|
|
679
763
|
```
|
|
680
764
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
### Debugging
|
|
765
|
+
The SDK skips the container when `enableContainer` is `false`, when the visitor opted
|
|
766
|
+
out, when `privacyMode` is `'strict'`, or when marketing consent is declined.
|
|
684
767
|
|
|
685
|
-
|
|
768
|
+
---
|
|
686
769
|
|
|
687
|
-
|
|
770
|
+
## Debugging
|
|
688
771
|
|
|
689
772
|
```typescript
|
|
690
773
|
getErrors(): ErrorInfo[]
|
|
@@ -704,40 +787,32 @@ interface ErrorInfo {
|
|
|
704
787
|
console.log(datalyr.getErrors());
|
|
705
788
|
```
|
|
706
789
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
---
|
|
710
|
-
|
|
711
|
-
### Lifecycle
|
|
712
|
-
|
|
713
|
-
#### `destroy()`
|
|
790
|
+
`getErrors()` returns the 50 most recent SDK errors. Set `debug: true` to log all SDK
|
|
791
|
+
activity to the console.
|
|
714
792
|
|
|
715
|
-
|
|
793
|
+
### destroy()
|
|
716
794
|
|
|
717
795
|
```typescript
|
|
718
796
|
destroy(): void
|
|
719
797
|
```
|
|
720
798
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
799
|
+
`destroy()` restores the patched `history.pushState` and `history.replaceState`, removes
|
|
800
|
+
every listener, and clears the queue, the session, container iframes, auto-identify
|
|
801
|
+
listeners, and encryption keys. Use it only when you tear the integration down for good.
|
|
724
802
|
|
|
725
803
|
---
|
|
726
804
|
|
|
727
|
-
## Web-to-
|
|
728
|
-
|
|
729
|
-
Track users who click a "Download App" button on your website and attribute their app install back to the original web session.
|
|
730
|
-
|
|
731
|
-
### Flow
|
|
805
|
+
## Web-to-app attribution
|
|
732
806
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
807
|
+
```typescript
|
|
808
|
+
trackAppDownloadClick(options: {
|
|
809
|
+
targetPlatform: 'ios' | 'android';
|
|
810
|
+
appStoreUrl: string;
|
|
811
|
+
}): Promise<void>
|
|
812
|
+
```
|
|
739
813
|
|
|
740
|
-
|
|
814
|
+
`trackAppDownloadClick()` fires a `$app_download_click` event with full attribution,
|
|
815
|
+
flushes through `sendBeacon`, then redirects to the store URL.
|
|
741
816
|
|
|
742
817
|
```javascript
|
|
743
818
|
document.querySelector('#download-btn').addEventListener('click', () => {
|
|
@@ -749,39 +824,45 @@ document.querySelector('#download-btn').addEventListener('click', () => {
|
|
|
749
824
|
```
|
|
750
825
|
|
|
751
826
|
```javascript
|
|
752
|
-
// Android - attribution params auto-encoded into Play Store referrer
|
|
753
827
|
datalyr.trackAppDownloadClick({
|
|
754
828
|
targetPlatform: 'android',
|
|
755
829
|
appStoreUrl: 'https://play.google.com/store/apps/details?id=com.yourapp',
|
|
756
830
|
});
|
|
757
831
|
```
|
|
758
832
|
|
|
759
|
-
|
|
833
|
+
For an Android Play Store URL, the SDK encodes the attribution parameters into the
|
|
834
|
+
`referrer` query parameter.
|
|
760
835
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
836
|
+
The mobile SDK recovers the web attribution on first app open:
|
|
837
|
+
|
|
838
|
+
| Platform | Match |
|
|
839
|
+
| --- | --- |
|
|
840
|
+
| Android | Deterministic, through the Play Store `referrer` parameter |
|
|
841
|
+
| iOS | IP-based, against web events from the last 24 hours |
|
|
842
|
+
|
|
843
|
+
This flow needs the Web SDK on the prelander page, and `@datalyr/react-native` or
|
|
844
|
+
`@datalyr/swift` in the app.
|
|
764
845
|
|
|
765
846
|
---
|
|
766
847
|
|
|
767
|
-
## SPA
|
|
848
|
+
## SPA support
|
|
768
849
|
|
|
769
|
-
|
|
850
|
+
The SDK tracks route changes while `trackSPA` is `true`, the default. It patches
|
|
851
|
+
`history.pushState` and `history.replaceState`, and listens for `popstate` and
|
|
852
|
+
`hashchange`. It clears the attribution cache on each navigation, so new URL parameters
|
|
853
|
+
are captured.
|
|
770
854
|
|
|
771
855
|
```javascript
|
|
772
856
|
datalyr.init({
|
|
773
857
|
workspaceId: 'YOUR_WORKSPACE_ID',
|
|
774
|
-
trackSPA: true,
|
|
775
|
-
trackPageViews: true,
|
|
858
|
+
trackSPA: true,
|
|
859
|
+
trackPageViews: true,
|
|
776
860
|
});
|
|
777
|
-
|
|
778
|
-
// Manual page tracking
|
|
779
|
-
datalyr.page({ title: 'Product Details', product_id: 'SKU-123' });
|
|
780
861
|
```
|
|
781
862
|
|
|
782
863
|
---
|
|
783
864
|
|
|
784
|
-
## Framework
|
|
865
|
+
## Framework integration
|
|
785
866
|
|
|
786
867
|
### React
|
|
787
868
|
|
|
@@ -825,6 +906,9 @@ const trackClick = () => {
|
|
|
825
906
|
|
|
826
907
|
### Next.js
|
|
827
908
|
|
|
909
|
+
SDK methods run in the browser only. Call them inside `useEffect` or another client-side
|
|
910
|
+
hook.
|
|
911
|
+
|
|
828
912
|
```tsx
|
|
829
913
|
// app/providers.tsx
|
|
830
914
|
'use client';
|
|
@@ -842,7 +926,9 @@ export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
|
|
|
842
926
|
|
|
843
927
|
return <>{children}</>;
|
|
844
928
|
}
|
|
929
|
+
```
|
|
845
930
|
|
|
931
|
+
```tsx
|
|
846
932
|
// app/layout.tsx
|
|
847
933
|
import { AnalyticsProvider } from './providers';
|
|
848
934
|
|
|
@@ -861,8 +947,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
861
947
|
|
|
862
948
|
## TypeScript
|
|
863
949
|
|
|
864
|
-
All types are exported from the package:
|
|
865
|
-
|
|
866
950
|
```typescript
|
|
867
951
|
import datalyr from '@datalyr/web';
|
|
868
952
|
import type {
|
|
@@ -882,9 +966,7 @@ import type {
|
|
|
882
966
|
ErrorInfo,
|
|
883
967
|
PerformanceMetrics,
|
|
884
968
|
} from '@datalyr/web';
|
|
885
|
-
```
|
|
886
969
|
|
|
887
|
-
```typescript
|
|
888
970
|
const properties: EventProperties = {
|
|
889
971
|
product_id: 'SKU-123',
|
|
890
972
|
price: 99.99,
|
|
@@ -900,7 +982,7 @@ const traits: UserTraits = {
|
|
|
900
982
|
datalyr.identify('user_123', traits);
|
|
901
983
|
```
|
|
902
984
|
|
|
903
|
-
### Plugin
|
|
985
|
+
### Plugin interface
|
|
904
986
|
|
|
905
987
|
```typescript
|
|
906
988
|
interface DatalyrPlugin {
|
|
@@ -915,56 +997,40 @@ interface DatalyrPlugin {
|
|
|
915
997
|
|
|
916
998
|
---
|
|
917
999
|
|
|
918
|
-
##
|
|
1000
|
+
## Verify the install
|
|
919
1001
|
|
|
920
|
-
|
|
1002
|
+
1. Open your published site in a private window.
|
|
1003
|
+
2. Open **Events** in Datalyr.
|
|
1004
|
+
3. Confirm a `pageview` arrives with your URL, within 30 seconds.
|
|
921
1005
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1006
|
+
When nothing arrives, set `debug: true`, then call `getErrors()` and `getNetworkStatus()`.
|
|
1007
|
+
|
|
1008
|
+
---
|
|
1009
|
+
|
|
1010
|
+
## Troubleshooting
|
|
927
1011
|
|
|
928
|
-
|
|
1012
|
+
| Symptom | Cause | Fix |
|
|
1013
|
+
| --- | --- | --- |
|
|
1014
|
+
| No events, network tab shows a `4xx` | Wrong `workspaceId`. The batch is dropped with no retry. | Copy the ID from **Settings → Tracking → Install** |
|
|
1015
|
+
| No events, no network request | `init()` never ran, or the visitor opted out | Call `datalyr.getWorkspaceId()` and `datalyr.isOptedOut()` |
|
|
1016
|
+
| No events, requests blocked | An ad blocker or a privacy extension | Test in a clean private window |
|
|
1017
|
+
| A session expires on every event | `sessionTimeout` was set in minutes | Set it in milliseconds: `3600000` for 60 minutes |
|
|
1018
|
+
| Attribution disappears after a few days | `attributionWindow` was set in days | Set it in milliseconds: `7776000000` for 90 days |
|
|
1019
|
+
| Two `visitor_id` values for one person | The script tag and the npm package both loaded | Remove one |
|
|
1020
|
+
| Conversions do not link to the visit | `reset()` runs on page load | Call `reset()` on logout only |
|
|
1021
|
+
| A Snapchat or Impact click is not attributed | Uppercase or alias parameter | The SDK maps `ScCid`, `sccid`, and `irclickid` on its own. Confirm the parameter reaches the landing URL. |
|
|
1022
|
+
| Stripe conversions are unattributed | A `checkout.stripe.com` link | Pass `getStripeMetadata()` from your server, or use a `buy.stripe.com` link |
|
|
1023
|
+
| `undefined` from every getter | `init()` never ran | Call `datalyr.init()` first |
|
|
929
1024
|
|
|
930
1025
|
```javascript
|
|
931
|
-
console.log('
|
|
1026
|
+
console.log('Workspace:', datalyr.getWorkspaceId());
|
|
1027
|
+
console.log('Visitor ID:', datalyr.getVisitorId());
|
|
932
1028
|
console.log('Session ID:', datalyr.getSessionId());
|
|
933
1029
|
console.log('User ID:', datalyr.getUserId());
|
|
934
1030
|
console.log('Network:', datalyr.getNetworkStatus());
|
|
935
1031
|
console.log('Errors:', datalyr.getErrors());
|
|
936
1032
|
```
|
|
937
1033
|
|
|
938
|
-
If these return `undefined`, the SDK is not initialized. Call `datalyr.init()` first.
|
|
939
|
-
|
|
940
|
-
### Async initialization
|
|
941
|
-
|
|
942
|
-
`init()` returns synchronously, but encryption and container loading happen asynchronously. If you need to ensure everything is ready before tracking:
|
|
943
|
-
|
|
944
|
-
```javascript
|
|
945
|
-
datalyr.init({ workspaceId: '...' });
|
|
946
|
-
await datalyr.ready();
|
|
947
|
-
datalyr.track('post_init_event');
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
### Next.js SSR
|
|
951
|
-
|
|
952
|
-
SDK methods only work in the browser. Always call them inside `useEffect` or other client-side hooks:
|
|
953
|
-
|
|
954
|
-
```jsx
|
|
955
|
-
'use client';
|
|
956
|
-
import { useEffect } from 'react';
|
|
957
|
-
import datalyr from '@datalyr/web';
|
|
958
|
-
|
|
959
|
-
useEffect(() => {
|
|
960
|
-
datalyr.init({ workspaceId: '...' });
|
|
961
|
-
}, []);
|
|
962
|
-
```
|
|
963
|
-
|
|
964
|
-
### Script tag vs NPM
|
|
965
|
-
|
|
966
|
-
Use one installation method, not both. Using both creates duplicate instances.
|
|
967
|
-
|
|
968
1034
|
---
|
|
969
1035
|
|
|
970
1036
|
## License
|