@datalyr/web 1.2.0 → 1.2.2

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 CHANGED
@@ -1,493 +1,496 @@
1
1
  # @datalyr/web
2
2
 
3
- The official Datalyr Web SDK for browser-based analytics. Track user behavior, measure conversions, and understand your customer journey with enterprise-grade analytics.
3
+ Browser analytics and attribution SDK for web applications. Track events, identify users, and capture attribution data across your website.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Quick Start](#quick-start)
9
+ - [How It Works](#how-it-works)
10
+ - [Configuration](#configuration)
11
+ - [Event Tracking](#event-tracking)
12
+ - [Custom Events](#custom-events)
13
+ - [Page Views](#page-views)
14
+ - [E-Commerce Events](#e-commerce-events)
15
+ - [User Identity](#user-identity)
16
+ - [Anonymous ID](#anonymous-id)
17
+ - [Identifying Users](#identifying-users)
18
+ - [User Properties](#user-properties)
19
+ - [Attribution](#attribution)
20
+ - [Automatic Capture](#automatic-capture)
21
+ - [Custom Parameters](#custom-parameters)
22
+ - [Event Queue](#event-queue)
23
+ - [Offline Support](#offline-support)
24
+ - [Privacy and Consent](#privacy-and-consent)
25
+ - [SPA Support](#spa-support)
26
+ - [Container Scripts](#container-scripts)
27
+ - [Framework Integration](#framework-integration)
28
+ - [React](#react)
29
+ - [Vue](#vue)
30
+ - [Next.js](#nextjs)
31
+ - [TypeScript](#typescript)
32
+ - [Troubleshooting](#troubleshooting)
33
+ - [License](#license)
4
34
 
5
- [![npm version](https://img.shields.io/npm/v/@datalyr/web.svg)](https://www.npmjs.com/package/@datalyr/web)
6
- [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@datalyr/web)](https://bundlephobia.com/package/@datalyr/web)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
8
- [![License](https://img.shields.io/npm/l/@datalyr/web.svg)](https://github.com/datalyr/web/blob/main/LICENSE)
9
-
10
- ## Features
11
-
12
- - **Zero Configuration** - Start tracking with just your workspace ID
13
- - **Lightweight** - ~15KB minified + gzipped with zero dependencies
14
- - **Privacy First** - GDPR compliant with built-in consent management
15
- - **Smart Batching** - Optimized network usage with intelligent event queuing
16
- - **Cross-Platform** - Works on all modern browsers including mobile
17
- - **Offline Support** - Events are queued and sent when connection restored
18
- - **Attribution Tracking** - Automatic UTM and click ID capture
19
- - **Session Management** - Automatic session tracking with configurable timeout
20
- - **TypeScript Support** - Full type definitions included
21
- - **Extensible** - Plugin system for custom functionality
22
- - **Container Scripts** - Securely manage third-party pixels and tracking scripts
35
+ ---
23
36
 
24
- ## Choosing the Right Installation Method
37
+ ## Installation
25
38
 
26
- Datalyr offers two installation methods. Choose based on your needs:
39
+ ### NPM Package
27
40
 
28
- ### Script Tag (Recommended for Most Cases)
41
+ ```bash
42
+ npm install @datalyr/web
43
+ ```
29
44
 
30
- **Use when:** Simple setup, zero bundle size, no build step needed
45
+ ### Script Tag
31
46
 
32
47
  ```html
33
- <script defer src="https://track.datalyr.com/container.js"
48
+ <script defer src="https://track.datalyr.com/dl.js"
34
49
  data-workspace-id="YOUR_WORKSPACE_ID"></script>
35
50
  ```
36
51
 
37
- **Pros:**
38
- - Zero bundle size (external script)
39
- - No npm install needed
40
- - Works in any HTML page
41
- - Automatic initialization
42
- - Perfect for Next.js, React, Vue
43
-
44
- **API Usage:**
45
- ```javascript
46
- // Available globally after script loads
47
- window.datalyr.track('event_name', { key: 'value' })
48
- window.datalyr.identify('user_123', { email: 'user@example.com' })
49
- window.datalyr.getVisitorId()
50
- ```
51
-
52
- [See Script Tag Documentation →](https://docs.datalyr.com/installation/script-tag)
52
+ The script tag loads externally (zero bundle size) and auto-initializes. Use `window.datalyr` to access the SDK.
53
53
 
54
54
  ---
55
55
 
56
- ### NPM Package (For TypeScript/Bundlers)
56
+ ## Quick Start
57
57
 
58
- **Use when:** You need TypeScript support, ES modules, or tree-shaking
58
+ ```javascript
59
+ import datalyr from '@datalyr/web';
59
60
 
60
- ```bash
61
- npm install @datalyr/web
62
- ```
61
+ // Initialize
62
+ datalyr.init({
63
+ workspaceId: 'YOUR_WORKSPACE_ID'
64
+ });
63
65
 
64
- **Pros:**
65
- - Full TypeScript definitions
66
- - ES module imports
67
- - Tree-shaking support
68
- - Better IDE autocomplete
66
+ // Track events
67
+ datalyr.track('button_clicked', { button: 'signup' });
69
68
 
70
- **API Usage:**
71
- ```javascript
72
- import datalyr from '@datalyr/web'
69
+ // Identify users
70
+ datalyr.identify('user_123', { email: 'user@example.com' });
73
71
 
74
- datalyr.init({ workspaceId: 'YOUR_WORKSPACE_ID' })
75
- datalyr.track('event_name', { key: 'value' })
76
- datalyr.identify('user_123', { email: 'user@example.com' })
72
+ // Track page views
73
+ datalyr.page('Pricing', { variant: 'A' });
77
74
  ```
78
75
 
79
76
  ---
80
77
 
81
- ### Important: Don't Mix Both Methods
78
+ ## How It Works
82
79
 
83
- Choose **one** installation method. Using both can cause conflicts:
80
+ The SDK collects events and sends them to the Datalyr backend for analytics and attribution.
84
81
 
85
- ```html
86
- <!-- DON'T DO THIS -->
87
- <script src="https://track.datalyr.com/container.js"></script>
88
- <script>
89
- import datalyr from '@datalyr/web' // Both methods at once!
90
- datalyr.init({...})
91
- </script>
92
- ```
82
+ ### Data Flow
93
83
 
94
- ## Installation
84
+ 1. Events are created with `track()`, `page()`, or `identify()`
85
+ 2. Each event includes device info, session data, and attribution parameters
86
+ 3. Events are queued locally and sent in batches
87
+ 4. If offline, events are stored and sent when connectivity returns
88
+ 5. Events are processed server-side for analytics and attribution reporting
95
89
 
96
- ```bash
97
- npm install @datalyr/web
98
- # or
99
- yarn add @datalyr/web
100
- # or
101
- pnpm add @datalyr/web
102
- ```
90
+ ### Event Payload
103
91
 
104
- ## Quick Start
92
+ Every event includes:
105
93
 
106
94
  ```javascript
107
- import datalyr from '@datalyr/web';
95
+ {
96
+ event: 'purchase', // Event name
97
+ properties: { ... }, // Custom properties
108
98
 
109
- // Initialize the SDK with your workspace ID
110
- datalyr.init({
111
- workspaceId: 'YOUR_WORKSPACE_ID'
112
- });
99
+ // Identity
100
+ anonymous_id: 'anon_xxxxx', // Persistent browser ID
101
+ user_id: 'user_123', // Set after identify()
102
+ session_id: 'sess_xxxxx', // Current session
113
103
 
114
- // Track an event
115
- datalyr.track('Button Clicked', {
116
- button_name: 'Sign Up',
117
- location: 'Header'
118
- });
104
+ // Context
105
+ page_url: 'https://example.com/pricing',
106
+ page_title: 'Pricing',
107
+ referrer: 'https://google.com',
119
108
 
120
- // Identify a user
121
- datalyr.identify('user_123', {
122
- email: 'user@example.com',
123
- name: 'John Doe',
124
- plan: 'premium'
125
- });
109
+ // Attribution (if captured)
110
+ utm_source: 'facebook',
111
+ fbclid: 'abc123',
112
+
113
+ // Timestamps
114
+ timestamp: '2024-01-15T10:30:00Z',
115
+ }
126
116
  ```
127
117
 
128
- ## Core Methods
118
+ ---
129
119
 
130
- ### Initialization
120
+ ## Configuration
131
121
 
132
122
  ```javascript
133
- import datalyr from '@datalyr/web';
134
-
135
- // Initialize the SDK with configuration
136
123
  datalyr.init({
137
- workspaceId: 'YOUR_WORKSPACE_ID',
124
+ // Required
125
+ workspaceId: string,
126
+
127
+ // Features
128
+ debug?: boolean, // Console logging
129
+ trackPageViews?: boolean, // Track initial page view (default: true)
130
+ trackSPA?: boolean, // Track SPA route changes (default: true)
131
+
132
+ // Event Queue
133
+ batchSize?: number, // Events per batch (default: 10)
134
+ flushInterval?: number, // Send interval ms (default: 5000)
135
+
136
+ // Session
137
+ sessionTimeout?: number, // Session timeout ms (default: 1800000 / 30 min)
138
+
139
+ // Privacy
140
+ privacyMode?: 'standard' | 'strict', // Privacy level (default: 'standard')
141
+ respectDoNotTrack?: boolean, // Honor browser DNT (default: false)
142
+ respectGlobalPrivacyControl?: boolean, // Honor GPC (default: true)
143
+
144
+ // Cookies
145
+ cookieDomain?: string | 'auto', // Cookie domain (default: 'auto')
138
146
 
139
- // Optional configuration
140
- debug: false, // Enable debug logging
141
- sessionTimeout: 1800000, // 30 minutes
142
- privacyMode: 'standard', // 'standard' or 'strict'
143
- respectDoNotTrack: false, // Honor browser DNT
144
- respectGlobalPrivacyControl: true, // Honor GPC
145
- cookieDomain: 'auto', // Auto-detect for subdomains
146
- trackSPA: true, // Track SPA route changes
147
- trackPageViews: true, // Track initial page view
148
- enableContainer: true // Enable container script loading
147
+ // Container
148
+ enableContainer?: boolean, // Load container scripts (default: true)
149
+
150
+ // Custom
151
+ trackedParams?: string[], // Additional URL params to track
152
+ plugins?: Plugin[], // Custom plugins
149
153
  });
150
154
  ```
151
155
 
152
- ### Track Events
156
+ ---
157
+
158
+ ## Event Tracking
159
+
160
+ ### Custom Events
153
161
 
154
- Track custom events with properties:
162
+ Track any action on your website:
155
163
 
156
164
  ```javascript
157
165
  // Simple event
158
- datalyr.track('Video Played');
166
+ datalyr.track('signup_started');
159
167
 
160
168
  // Event with properties
161
- datalyr.track('Product Viewed', {
162
- product_id: 'SKU-123',
163
- product_name: 'Premium Widget',
164
- category: 'Electronics',
165
- price: 99.99,
166
- currency: 'USD'
169
+ datalyr.track('product_viewed', {
170
+ product_id: 'SKU123',
171
+ product_name: 'Blue Shirt',
172
+ price: 29.99,
173
+ currency: 'USD',
174
+ category: 'Apparel',
167
175
  });
168
176
 
169
- // E-commerce events
170
- datalyr.track('Purchase', {
171
- order_id: 'ORD-456',
172
- total: 299.99,
173
- currency: 'USD',
174
- items: [
175
- { product_id: 'SKU-123', quantity: 2, price: 99.99 },
176
- { product_id: 'SKU-456', quantity: 1, price: 100.01 }
177
- ]
177
+ // Event with value
178
+ datalyr.track('level_completed', {
179
+ level: 5,
180
+ score: 1250,
181
+ time_seconds: 120,
178
182
  });
179
183
  ```
180
184
 
181
- ### Identify Users
185
+ ### Page Views
182
186
 
183
- Associate events with users:
187
+ Track navigation:
184
188
 
185
189
  ```javascript
186
- // Identify with user ID only
187
- datalyr.identify('user_123');
190
+ // Track current page
191
+ datalyr.page();
188
192
 
189
- // Identify with traits
190
- datalyr.identify('user_123', {
191
- email: 'user@example.com',
192
- name: 'John Doe',
193
- firstName: 'John',
194
- lastName: 'Doe',
195
- phone: '+1234567890',
196
- plan: 'premium',
197
- createdAt: '2024-01-01'
198
- });
193
+ // Track with name
194
+ datalyr.page('Pricing');
199
195
 
200
- // Update user traits without changing ID
201
- datalyr.identify(null, {
202
- plan: 'enterprise',
203
- company: 'Acme Corp'
196
+ // Track with properties
197
+ datalyr.page('Product Details', {
198
+ product_id: 'SKU123',
199
+ source: 'search',
204
200
  });
205
201
  ```
206
202
 
207
- ### Page Tracking
203
+ ### E-Commerce Events
208
204
 
209
- Track page views:
205
+ Standard e-commerce tracking:
210
206
 
211
207
  ```javascript
212
- // Track current page
213
- datalyr.page();
208
+ // View product
209
+ datalyr.track('Product Viewed', {
210
+ product_id: 'SKU123',
211
+ product_name: 'Blue Shirt',
212
+ price: 29.99,
213
+ currency: 'USD'
214
+ });
214
215
 
215
- // Track with custom properties
216
- datalyr.page({
217
- title: 'Pricing Page',
218
- category: 'Marketing',
219
- author: 'Marketing Team'
216
+ // Add to cart
217
+ datalyr.track('Add to Cart', {
218
+ product_id: 'SKU123',
219
+ quantity: 1,
220
+ price: 29.99
220
221
  });
221
222
 
222
- // Track with page name
223
- datalyr.page('Pricing', {
224
- plan: 'premium',
225
- variant: 'A'
223
+ // Start checkout
224
+ datalyr.track('Checkout Started', {
225
+ cart_value: 59.98,
226
+ currency: 'USD',
227
+ item_count: 2
228
+ });
229
+
230
+ // Complete purchase
231
+ datalyr.track('Purchase', {
232
+ order_id: 'ORD-456',
233
+ total: 59.98,
234
+ currency: 'USD',
235
+ items: [
236
+ { product_id: 'SKU123', quantity: 2, price: 29.99 }
237
+ ]
226
238
  });
227
239
  ```
228
240
 
229
- ### Managing Identity
241
+ ---
230
242
 
231
- ```javascript
232
- // Get current user IDs
233
- const anonymousId = datalyr.getAnonymousId();
234
- const userId = datalyr.getUserId();
235
- const distinctId = datalyr.getDistinctId();
243
+ ## User Identity
236
244
 
237
- // Reset identity (for logout)
238
- datalyr.reset();
245
+ ### Anonymous ID
246
+
247
+ Every visitor gets a persistent anonymous ID on first visit:
239
248
 
240
- // Create alias (link anonymous to identified user)
241
- datalyr.alias('user_123');
249
+ ```javascript
250
+ const anonymousId = datalyr.getAnonymousId();
251
+ // 'anon_a1b2c3d4e5f6'
242
252
  ```
243
253
 
244
- ### User Consent
254
+ This ID:
255
+ - Persists across browser sessions
256
+ - Links events before and after user identification
257
+ - Can be passed to your backend for server-side attribution
258
+
259
+ ### Identifying Users
245
260
 
246
- Manage user privacy preferences:
261
+ Link the anonymous ID to a known user:
247
262
 
248
263
  ```javascript
249
- // Opt user out of tracking
250
- datalyr.optOut();
264
+ datalyr.identify('user_123', {
265
+ email: 'user@example.com',
266
+ });
267
+ ```
251
268
 
252
- // Opt user back in
253
- datalyr.optIn();
269
+ After `identify()`:
270
+ - All future events include `user_id`
271
+ - Historical anonymous events can be linked server-side
272
+ - Attribution data is preserved on the user
254
273
 
255
- // Check opt-out status
256
- const isOptedOut = datalyr.isOptedOut();
274
+ ### User Properties
257
275
 
258
- // Set consent preferences
259
- datalyr.setConsent({
260
- analytics: true,
261
- marketing: false,
262
- preferences: true,
263
- sale: false // CCPA "Do Not Sell"
276
+ Pass any user attributes:
277
+
278
+ ```javascript
279
+ datalyr.identify('user_123', {
280
+ // Standard fields
281
+ email: 'user@example.com',
282
+ name: 'John Doe',
283
+ phone: '+1234567890',
284
+
285
+ // Custom fields
286
+ plan: 'premium',
287
+ company: 'Acme Inc',
288
+ signup_date: '2024-01-15',
264
289
  });
265
290
  ```
266
291
 
267
- ### Session Management
292
+ ### Logout
293
+
294
+ Clear user data on logout:
268
295
 
269
296
  ```javascript
270
- // Get current session ID
271
- const sessionId = datalyr.getSessionId();
297
+ datalyr.reset();
298
+ ```
272
299
 
273
- // Start new session
274
- datalyr.startNewSession();
300
+ This:
301
+ - Clears the user ID
302
+ - Starts a new session
303
+ - Keeps the anonymous ID (same browser)
275
304
 
276
- // Get session data
277
- const sessionData = datalyr.getSessionData();
278
- ```
305
+ ---
279
306
 
280
- ## Advanced Features
307
+ ## Attribution
281
308
 
282
- ### Event Priority
309
+ ### Automatic Capture
283
310
 
284
- Important events like purchases are automatically prioritized for faster delivery:
311
+ The SDK captures attribution from URLs and referrers:
285
312
 
286
313
  ```javascript
287
- // Important conversion events
288
- datalyr.track('Purchase', { value: 99.99 });
289
- datalyr.track('Signup', { plan: 'premium' });
290
- datalyr.track('Subscribe', { plan: 'annual' });
291
-
292
- // E-commerce events
293
- datalyr.track('Add to Cart', { product_id: 'SKU-123' });
294
- datalyr.track('Begin Checkout', { cart_value: 199.99 });
314
+ const attribution = datalyr.getAttributionData();
295
315
  ```
296
316
 
297
- ### Attribution Tracking
317
+ Captured parameters:
298
318
 
299
- The SDK automatically captures:
300
- - UTM parameters (utm_source, utm_medium, utm_campaign, utm_term, utm_content)
301
- - Click IDs (gclid, fbclid, ttclid, msclkid, twclid, li_fat_id)
302
- - Referrer information
303
- - Landing page
304
- - Customer journey touchpoints
319
+ | Type | Parameters |
320
+ |------|------------|
321
+ | UTM | `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term` |
322
+ | Click IDs | `fbclid`, `gclid`, `ttclid`, `twclid`, `li_fat_id`, `msclkid` |
323
+ | Referrer | `referrer`, `landing_page` |
305
324
 
306
- ```javascript
307
- // Attribution data is automatically added to events
308
- datalyr.track('Signup');
309
- // Includes: utm_source, utm_medium, first_touch_source, last_touch_source, etc.
325
+ Attribution is automatically included in all events and supports:
326
+ - First-touch attribution (90-day window)
327
+ - Last-touch attribution
328
+ - Customer journey tracking (up to 30 touchpoints)
310
329
 
311
- // Track custom URL parameters
312
- import datalyr from '@datalyr/web';
330
+ ### Custom Parameters
313
331
 
332
+ Track additional URL parameters:
333
+
334
+ ```javascript
314
335
  datalyr.init({
315
336
  workspaceId: 'YOUR_WORKSPACE_ID',
316
337
  trackedParams: ['ref', 'affiliate_id', 'promo_code']
317
338
  });
318
339
  ```
319
340
 
320
- ### SPA Support
341
+ ---
321
342
 
322
- For single-page applications:
343
+ ## Event Queue
323
344
 
324
- ```javascript
325
- import datalyr from '@datalyr/web';
345
+ Events are batched for efficiency.
346
+
347
+ ### Configuration
326
348
 
349
+ ```javascript
327
350
  datalyr.init({
328
351
  workspaceId: 'YOUR_WORKSPACE_ID',
329
- trackSPA: true // Auto-track route changes (default: true)
330
- });
331
-
332
- // Manual route tracking
333
- datalyr.page('Product Details', {
334
- product_id: 'SKU-123'
352
+ batchSize: 10, // Send when 10 events queued
353
+ flushInterval: 5000, // Or every 5 seconds
335
354
  });
336
355
  ```
337
356
 
338
- ### Privacy Modes
357
+ ### Manual Flush
339
358
 
340
- Control data collection:
359
+ Send all queued events immediately:
341
360
 
342
361
  ```javascript
343
- import datalyr from '@datalyr/web';
362
+ await datalyr.flush();
363
+ ```
344
364
 
345
- // Standard mode (default) - balanced privacy
346
- datalyr.init({
347
- workspaceId: 'YOUR_WORKSPACE_ID',
348
- privacyMode: 'standard'
349
- });
365
+ ### Priority Events
350
366
 
351
- // Or strict mode - minimal data collection
352
- datalyr.init({
353
- workspaceId: 'YOUR_WORKSPACE_ID',
354
- privacyMode: 'strict'
355
- });
356
- ```
367
+ Important events like `Purchase`, `Signup`, and `Subscribe` are automatically prioritized for faster delivery.
357
368
 
358
- ### Offline Support
369
+ ---
359
370
 
360
- Events are automatically saved when offline and sent when connection is restored:
371
+ ## Offline Support
372
+
373
+ When the browser is offline:
374
+ - Events are stored locally
375
+ - Queue persists across page refreshes
376
+ - Events are sent when connectivity returns
361
377
 
362
378
  ```javascript
363
379
  // Events work seamlessly offline
364
380
  datalyr.track('Button Clicked'); // Automatically queued if offline
365
381
 
366
- // Manually flush pending events
382
+ // Manually flush when ready
367
383
  await datalyr.flush();
368
384
  ```
369
385
 
370
- ### Cross-Subdomain Tracking
386
+ ---
387
+
388
+ ## Privacy and Consent
371
389
 
372
- Automatically track users across subdomains:
390
+ ### Opt Out
373
391
 
374
392
  ```javascript
375
- import datalyr from '@datalyr/web';
393
+ // Opt user out of tracking
394
+ datalyr.optOut();
395
+
396
+ // Opt user back in
397
+ datalyr.optIn();
398
+
399
+ // Check status
400
+ const isOptedOut = datalyr.isOptedOut();
401
+ ```
402
+
403
+ ### Consent Preferences
376
404
 
377
- // Auto-detect domain for *.example.com
405
+ ```javascript
406
+ datalyr.setConsent({
407
+ analytics: true,
408
+ marketing: false,
409
+ preferences: true,
410
+ sale: false // CCPA "Do Not Sell"
411
+ });
412
+ ```
413
+
414
+ ### Privacy Modes
415
+
416
+ ```javascript
417
+ // Standard mode (default)
378
418
  datalyr.init({
379
419
  workspaceId: 'YOUR_WORKSPACE_ID',
380
- cookieDomain: 'auto' // Default
420
+ privacyMode: 'standard'
381
421
  });
382
422
 
383
- // Or set explicitly
423
+ // Strict mode - minimal data collection
384
424
  datalyr.init({
385
425
  workspaceId: 'YOUR_WORKSPACE_ID',
386
- cookieDomain: '.example.com'
426
+ privacyMode: 'strict'
387
427
  });
388
428
  ```
389
429
 
390
- ### Container Scripts
430
+ ---
391
431
 
392
- The SDK can automatically load and manage third-party tracking scripts with built-in security:
432
+ ## SPA Support
393
433
 
394
- ```javascript
395
- import datalyr from '@datalyr/web';
434
+ For single-page applications:
396
435
 
436
+ ```javascript
397
437
  datalyr.init({
398
438
  workspaceId: 'YOUR_WORKSPACE_ID',
399
- enableContainer: true // Default: true
439
+ trackSPA: true // Auto-track route changes (default: true)
400
440
  });
401
441
 
402
- // Container scripts are loaded automatically based on your
403
- // configuration in the Datalyr dashboard
404
-
405
- // Manually trigger a custom script
406
- datalyr.loadScript('custom-script-id');
407
-
408
- // Get list of loaded scripts
409
- const loadedScripts = datalyr.getLoadedScripts();
442
+ // Manual route tracking
443
+ datalyr.page('Product Details', {
444
+ product_id: 'SKU-123'
445
+ });
410
446
  ```
411
447
 
412
- The container manager supports:
413
- - **Third-party pixels**: Meta (Facebook), Google, TikTok
414
- - **Custom scripts**: Inline JavaScript or external scripts
415
- - **Tracking pixels**: Image-based tracking pixels
416
- - **Conditional loading**: Based on URL, device type, etc.
417
- - **Frequency control**: Once per session, once per page, or always
418
-
419
- **Security Features:**
420
- - HTTPS-only script loading (except localhost)
421
- - XSS protection with pattern detection
422
- - URL validation and protocol blocking
423
- - CSP nonce support for inline scripts
424
- - Automatic async loading for better performance
425
-
426
- ### Plugins
448
+ ---
427
449
 
428
- Extend SDK functionality:
450
+ ## Container Scripts
429
451
 
430
- ```javascript
431
- import datalyr from '@datalyr/web';
452
+ Container scripts manage third-party tracking pixels (Meta, Google, TikTok) configured in your Datalyr dashboard.
432
453
 
433
- // Create a plugin
434
- const myPlugin = {
435
- name: 'my-plugin',
454
+ ### Script Tag Installation
436
455
 
437
- initialize(datalyr) {
438
- console.log('Plugin initialized');
439
- },
456
+ For container scripts, use the container endpoint:
440
457
 
441
- track(eventName, properties) {
442
- console.log('Event tracked:', eventName);
443
- },
458
+ ```html
459
+ <script defer src="https://track.datalyr.com/container.js"
460
+ data-workspace-id="YOUR_WORKSPACE_ID"></script>
461
+ ```
444
462
 
445
- identify(userId, traits) {
446
- console.log('User identified:', userId);
447
- },
463
+ This loads your configured pixels and tracking scripts automatically.
448
464
 
449
- page(properties) {
450
- console.log('Page tracked');
451
- }
452
- };
465
+ ### NPM SDK with Containers
453
466
 
454
- // Register plugin during initialization
467
+ ```javascript
455
468
  datalyr.init({
456
469
  workspaceId: 'YOUR_WORKSPACE_ID',
457
- plugins: [myPlugin]
470
+ enableContainer: true // Default: true
458
471
  });
459
- ```
460
472
 
461
- ## TypeScript Support
462
-
463
- Full TypeScript definitions are included:
464
-
465
- ```typescript
466
- import datalyr, { EventProperties, UserTraits } from '@datalyr/web';
473
+ // Manually trigger a script
474
+ datalyr.loadScript('custom-script-id');
467
475
 
468
- // Initialize the SDK
469
- datalyr.init({
470
- workspaceId: 'YOUR_WORKSPACE_ID'
471
- });
476
+ // Get loaded scripts
477
+ const scripts = datalyr.getLoadedScripts();
478
+ ```
472
479
 
473
- // Type-safe event properties
474
- const properties: EventProperties = {
475
- product_id: 'SKU-123',
476
- price: 99.99,
477
- quantity: 2
478
- };
480
+ ### Supported Pixels
479
481
 
480
- datalyr.track('Product Added', properties);
482
+ - Meta (Facebook) Pixel
483
+ - Google Analytics / Google Ads
484
+ - TikTok Pixel
485
+ - Custom scripts and tracking pixels
481
486
 
482
- // Type-safe user traits
483
- const traits: UserTraits = {
484
- email: 'user@example.com',
485
- name: 'John Doe',
486
- plan: 'premium'
487
- };
487
+ Security features:
488
+ - HTTPS-only script loading
489
+ - XSS protection
490
+ - URL validation
491
+ - CSP nonce support
488
492
 
489
- datalyr.identify('user_123', traits);
490
- ```
493
+ ---
491
494
 
492
495
  ## Framework Integration
493
496
 
@@ -505,9 +508,7 @@ function App() {
505
508
  }, []);
506
509
 
507
510
  const handleClick = () => {
508
- datalyr.track('Button Clicked', {
509
- button_name: 'CTA'
510
- });
511
+ datalyr.track('Button Clicked', { button_name: 'CTA' });
511
512
  };
512
513
 
513
514
  return <button onClick={handleClick}>Click Me</button>;
@@ -552,14 +553,6 @@ export function AnalyticsProvider({ children }) {
552
553
  workspaceId: process.env.NEXT_PUBLIC_DATALYR_WORKSPACE_ID,
553
554
  debug: process.env.NODE_ENV === 'development'
554
555
  });
555
-
556
- // Verify initialization
557
- console.log('Datalyr initialized');
558
- console.log('Session ID:', datalyr.getSessionId());
559
- console.log('Anonymous ID:', datalyr.getAnonymousId());
560
-
561
- // Track initial page view
562
- datalyr.page();
563
556
  }, []);
564
557
 
565
558
  return children;
@@ -581,518 +574,84 @@ export default function RootLayout({ children }) {
581
574
  }
582
575
  ```
583
576
 
584
- ## Authentication & Attribution Tracking
585
-
586
- ### Critical: Client-Side Identification for Attribution
587
-
588
- When implementing authentication flows (login, signup, OAuth), you **MUST** call identify on the client-side (browser) to preserve attribution data. Server-side identification creates new sessions and loses all attribution context.
589
-
590
- #### The Problem with Server-Side Identify
591
-
592
- ```javascript
593
- // ❌ WRONG - Server-side identify breaks attribution
594
- app.post('/api/login', async (req, res) => {
595
- const user = await authenticate(email, password);
596
-
597
- // This creates a NEW session without attribution data!
598
- await serverAnalytics.identify({
599
- userId: user.id,
600
- traits: { email: user.email }
601
- });
602
-
603
- res.json({ userId: user.id });
604
- });
605
- // Result: Lost UTM params, fbclid, gclid, referrer - all gone!
606
- ```
607
-
608
- #### The Correct Solution
609
-
610
- ```javascript
611
- // ✅ CORRECT - Client-side identification preserves attribution
612
- // React/Next.js example
613
- function LoginPage() {
614
- const handleLogin = async () => {
615
- // 1. Authenticate on server
616
- const response = await fetch('/api/login', {
617
- method: 'POST',
618
- body: JSON.stringify({ email, password })
619
- });
620
-
621
- const { userId, email, name } = await response.json();
622
-
623
- // 2. Identify in browser - this preserves attribution!
624
- datalyr.identify(userId, {
625
- email: email,
626
- name: name,
627
- login_method: 'email'
628
- });
629
-
630
- // 3. Then redirect
631
- router.push('/dashboard');
632
- };
633
- }
634
-
635
- // Server endpoint - NO identify, just auth
636
- app.post('/api/login', async (req, res) => {
637
- const user = await authenticate(email, password);
638
-
639
- // Just return user data for client-side identify
640
- res.json({
641
- userId: user.id,
642
- email: user.email,
643
- name: user.name
644
- });
645
- });
646
- ```
647
-
648
- ### Why Client-Side Identification Matters
649
-
650
- 1. **Attribution Preserved**: UTM params, fbclid, gclid, referrer all stay linked to the user
651
- 2. **Session Continuity**: Links anonymous session (with all its context) to identified user
652
- 3. **Cookie Access**: Can read and maintain browser session data
653
- 4. **Accurate Journey**: Tracks the complete anonymous → identified user journey
654
-
655
- ### Implementation Checklist
656
-
657
- Ensure you call identify **client-side** in these scenarios:
658
-
659
- - [ ] **After Signup Success**: When account creation API returns success
660
- - [ ] **After Login Success**: When authentication API returns success
661
- - [ ] **After OAuth Redirect**: When returning from OAuth provider
662
- - [ ] **After Magic Link Click**: When magic link is validated
663
- - [ ] **On App Load**: If user session exists, re-identify
664
- - [ ] **After Email Verification**: When user verifies their email
665
- - [ ] **Session Restore**: When detecting existing authenticated session
666
-
667
- ### Example: Complete OAuth Implementation
668
-
669
- ```javascript
670
- // Client-side: After OAuth redirect
671
- useEffect(() => {
672
- // Check if we just came back from OAuth
673
- if (window.location.search.includes('oauth=success')) {
674
- // Get user data from your API/session
675
- fetch('/api/me')
676
- .then(res => res.json())
677
- .then(user => {
678
- // Identify in browser - preserves attribution!
679
- datalyr.identify(user.id, {
680
- email: user.email,
681
- name: user.name,
682
- auth_method: 'google',
683
- signup_source: localStorage.getItem('signup_source') || 'organic'
684
- });
685
-
686
- // Track the appropriate event
687
- if (user.isNewUser) {
688
- datalyr.track('Signup Completed', { method: 'oauth' });
689
- } else {
690
- datalyr.track('Login Successful', { method: 'oauth' });
691
- }
692
- });
693
- }
694
- }, []);
695
-
696
- // Server-side: OAuth callback - NO identify!
697
- export async function GET(request: Request) {
698
- const { user } = await validateOAuthCallback(request);
699
-
700
- const existingUser = await getUserByEmail(user.email);
701
-
702
- if (existingUser) {
703
- // Set session, but DON'T identify
704
- await createSession(existingUser.id);
705
- return redirect('/dashboard?oauth=success');
706
- } else {
707
- // Create user, but DON'T identify
708
- const newUser = await createUser(user);
709
- await createSession(newUser.id);
710
- return redirect('/dashboard?oauth=success&new=true');
711
- }
712
- }
713
- ```
714
-
715
- ### Server-Side Events (Different from Identify!)
716
-
717
- Use server-side for **tracking events** with userId (not identify):
718
-
719
- ```javascript
720
- // ✅ CORRECT - Server-side events with userId
721
- // Stripe webhook example
722
- app.post('/webhook/stripe', async (req, res) => {
723
- const event = stripe.webhooks.constructEvent(req.body, sig, secret);
724
-
725
- if (event.type === 'checkout.session.completed') {
726
- const session = event.data.object;
727
-
728
- // Track purchase event with userId (NOT identify)
729
- await analytics.track({
730
- userId: session.client_reference_id, // Your user ID
731
- event: 'Purchase Completed',
732
- properties: {
733
- amount: session.amount_total / 100,
734
- currency: session.currency,
735
- plan: session.metadata.plan
736
- }
737
- });
738
- }
739
- });
740
- ```
741
-
742
- ### What Happens with Server-Side Identify (Wrong!)
743
-
744
- If you identify server-side:
745
- - **Lost Attribution**: New session created, no UTM params or click IDs
746
- - **Broken Journey**: Can't link anonymous browsing to identified user
747
- - **Multiple Sessions**: Each server identify creates disconnected session
748
- - **No Context**: Missing referrer, landing page, device info
749
-
750
- ## Best Practices
751
-
752
- ### 1. Initialize Early
753
- Initialize the SDK as early as possible in your application lifecycle:
754
-
755
- ```javascript
756
- // In your main entry file
757
- import datalyr from '@datalyr/web';
758
-
759
- // Initialize before other code
760
- datalyr.init({
761
- workspaceId: 'YOUR_WORKSPACE_ID'
762
- });
763
-
764
- // Your app code...
765
- ```
766
-
767
- ### 2. Use Consistent Event Naming
768
- Adopt a naming convention for your events:
577
+ ---
769
578
 
770
- ```javascript
771
- // Good: Consistent, descriptive names
772
- datalyr.track('Product Viewed');
773
- datalyr.track('Cart Updated');
774
- datalyr.track('Checkout Started');
775
-
776
- // Avoid: Inconsistent naming
777
- datalyr.track('view_product');
778
- datalyr.track('Cart-Update');
779
- datalyr.track('CHECKOUT_START');
780
- ```
579
+ ## TypeScript
781
580
 
782
- ### 3. Include Relevant Properties
783
- Add properties that will help with analysis:
581
+ ```typescript
582
+ import datalyr, { EventProperties, UserTraits } from '@datalyr/web';
784
583
 
785
- ```javascript
786
- // Good: Rich context
787
- datalyr.track('Product Viewed', {
584
+ // Type-safe event properties
585
+ const properties: EventProperties = {
788
586
  product_id: 'SKU-123',
789
- product_name: 'Premium Widget',
790
- category: 'Electronics',
791
587
  price: 99.99,
792
- currency: 'USD',
793
- in_stock: true,
794
- view_type: 'quick_view'
795
- });
796
-
797
- // Avoid: Missing context
798
- datalyr.track('Product Viewed', {
799
- id: '123'
800
- });
801
- ```
802
-
803
- ### 4. ALWAYS Identify in the Browser (Not Server)
804
- This is critical for attribution:
805
-
806
- ```javascript
807
- // ✅ CORRECT - Browser identification
808
- async function handleLogin(email, password) {
809
- const response = await fetch('/api/login', {
810
- method: 'POST',
811
- body: JSON.stringify({ email, password })
812
- });
813
-
814
- const user = await response.json();
815
-
816
- // Identify in browser - preserves attribution!
817
- datalyr.identify(user.id, {
818
- email: user.email,
819
- name: user.name,
820
- plan: user.subscription
821
- });
822
- }
588
+ quantity: 2
589
+ };
823
590
 
824
- // WRONG - Server identification
825
- // This breaks attribution tracking!
826
- app.post('/api/login', async (req, res) => {
827
- const user = await authenticate(req.body);
591
+ datalyr.track('Product Added', properties);
828
592
 
829
- // DON'T DO THIS - Creates new session
830
- await serverAnalytics.identify(user.id, {
831
- signup_date: new Date().toISOString()
832
- });
593
+ // Type-safe user traits
594
+ const traits: UserTraits = {
595
+ email: 'user@example.com',
596
+ name: 'John Doe',
597
+ plan: 'premium'
598
+ };
833
599
 
834
- res.json({ success: true });
835
- });
600
+ datalyr.identify('user_123', traits);
836
601
  ```
837
602
 
838
- ### 5. Handle Errors Gracefully
839
- The SDK handles errors internally, but you can add your own error tracking:
840
-
841
- ```javascript
842
- window.addEventListener('error', (event) => {
843
- datalyr.track('JavaScript Error', {
844
- message: event.message,
845
- source: event.filename,
846
- line: event.lineno,
847
- column: event.colno,
848
- error: event.error?.toString()
849
- });
850
- });
851
- ```
603
+ ---
852
604
 
853
605
  ## Troubleshooting
854
606
 
855
- ### Events Not Showing in Dashboard?
607
+ ### Events not appearing
856
608
 
857
- **Step 1: Verify SDK is initialized**
858
- ```javascript
859
- import datalyr from '@datalyr/web'
609
+ 1. Check workspace ID is correct
610
+ 2. Enable `debug: true`
611
+ 3. Verify `datalyr.getAnonymousId()` returns a value
612
+ 4. Check Network tab for requests to `ingest.datalyr.com`
613
+ 5. Disable ad blockers
860
614
 
861
- // Check these return values
862
- console.log('Anonymous ID:', datalyr.getAnonymousId()) // Should return "anon_xxxxx"
863
- console.log('Session ID:', datalyr.getSessionId()) // Should return "sess_xxxxx"
864
- ```
865
-
866
- If these return `undefined`, the SDK isn't initialized properly.
615
+ ### SDK not initialized
867
616
 
868
- **Step 2: Check for errors**
869
617
  ```javascript
870
- console.log('Errors:', datalyr.getErrors()) // Should be empty [] or show errors
618
+ // Check initialization
619
+ console.log('Anonymous ID:', datalyr.getAnonymousId());
620
+ console.log('Session ID:', datalyr.getSessionId());
621
+ console.log('Errors:', datalyr.getErrors());
871
622
  ```
872
623
 
873
- **Step 3: Enable debug mode**
874
- ```javascript
875
- datalyr.init({
876
- workspaceId: 'YOUR_WORKSPACE_ID',
877
- debug: true // Shows [Datalyr] logs in console
878
- })
879
- ```
880
-
881
- **Step 4: Verify network requests**
882
- 1. Open DevTools → Network tab
883
- 2. Filter by "ingest"
884
- 3. Track a test event: `datalyr.track('test', {})`
885
- 4. You should see POST to `ingest.datalyr.com`
886
-
887
- ### Common Errors
888
-
889
- **"Cannot find module '@datalyr/web'"**
890
- - Run `npm install @datalyr/web`
891
- - Restart your dev server
892
-
893
- **"process.env.NEXT_PUBLIC_DATALYR_WORKSPACE_ID is undefined"**
894
- - Create `.env.local` file in project root
895
- - Add: `NEXT_PUBLIC_DATALYR_WORKSPACE_ID=your_id`
896
- - Restart dev server (required for env changes)
897
-
898
- **"datalyr.getAnonymousId() returns undefined"**
899
- - You're calling it during SSR (server-side rendering)
900
- - Make sure you're in a Client Component with `'use client'`
901
- - Call it inside `useEffect` or after component mounts
902
-
903
- **Events tracked but not in dashboard**
904
- - Verify workspace ID matches your dashboard
905
- - Events may take 1-2 minutes to appear
906
- - Check Network tab - requests should return 200 OK
907
- - Check if ad blocker is enabled (disable it)
908
-
909
- ### Script Tag vs SDK Confusion
910
-
911
- **If you're using the script tag:**
912
- ```javascript
913
- // Check if loaded
914
- window.datalyr._isLoaded // Should be true
915
- window.datalyr.getVisitorId() // Should return ID
916
-
917
- // Use object methods, not function calls
918
- window.datalyr.track('event', {}) // ✅ Correct
919
- window.datalyr("track", "event", {}) // ❌ Wrong
920
- ```
921
-
922
- **If you're using the NPM SDK:**
923
- ```javascript
924
- import datalyr from '@datalyr/web'
925
-
926
- // Check if loaded
927
- datalyr.getAnonymousId() // Should return ID
928
- datalyr.getSessionId() // Should return ID
929
- ```
624
+ If these return `undefined`, the SDK isn't initialized.
930
625
 
931
- ## Migration from Script Tag
626
+ ### Next.js SSR issues
932
627
 
933
- If you're currently using the Datalyr script tag and want to switch to the NPM package:
628
+ Make sure you're calling SDK methods client-side only:
934
629
 
935
- ### Before (Script Tag):
936
- ```html
937
- <!-- In your HTML -->
938
- <script defer src="https://track.datalyr.com/container.js"
939
- data-workspace-id="YOUR_WORKSPACE_ID"></script>
940
- ```
941
-
942
- ```javascript
943
- // In your JavaScript
944
- window.datalyr.track('Button Clicked', {
945
- button_name: 'Sign Up'
946
- });
947
-
948
- window.datalyr.identify('user_123', {
949
- email: 'user@example.com'
950
- });
951
- ```
952
-
953
- ### After (NPM SDK):
954
- ```javascript
955
- // Remove the script tag from your HTML
956
-
957
- // Install the package
958
- // npm install @datalyr/web
630
+ ```jsx
631
+ 'use client';
959
632
 
960
- // In your app initialization
633
+ import { useEffect } from 'react';
961
634
  import datalyr from '@datalyr/web';
962
635
 
963
- datalyr.init({
964
- workspaceId: 'YOUR_WORKSPACE_ID'
965
- });
966
-
967
- // In your components
968
- datalyr.track('Button Clicked', {
969
- button_name: 'Sign Up'
970
- });
971
-
972
- datalyr.identify('user_123', {
973
- email: 'user@example.com'
974
- });
636
+ useEffect(() => {
637
+ // SDK methods only work in browser
638
+ datalyr.init({ workspaceId: '...' });
639
+ }, []);
975
640
  ```
976
641
 
977
- **Note:** Most users should stay with the script tag unless they specifically need TypeScript support or ES module imports. The script tag has zero bundle size and works perfectly with Next.js, React, and Vue.
978
-
979
- ## API Comparison: Script Tag vs NPM SDK
980
-
981
- | Feature | Script Tag | NPM SDK |
982
- |---------|-----------|---------|
983
- | **Installation** | Add `<script>` to HTML | `npm install @datalyr/web` |
984
- | **Bundle Size** | 0 (external) | ~15KB |
985
- | **TypeScript** | No types | ✅ Full types |
986
- | **Import** | `window.datalyr` | `import datalyr from '@datalyr/web'` |
987
- | **Initialize** | Automatic | `datalyr.init({...})` |
988
- | **Track Event** | `window.datalyr.track(...)` | `datalyr.track(...)` |
989
- | **Identify** | `window.datalyr.identify(...)` | `datalyr.identify(...)` |
990
- | **Get Visitor ID** | `window.datalyr.getVisitorId()` | `datalyr.getAnonymousId()` |
991
- | **Get Session** | `window.datalyr._getSessionId()` | `datalyr.getSessionId()` |
992
- | **Check Loaded** | `window.datalyr._isLoaded` | `datalyr.getAnonymousId() !== undefined` |
993
-
994
- Both methods use the same tracking backend and provide identical attribution tracking.
995
-
996
- ## Browser Support
997
-
998
- The SDK supports all modern browsers:
999
- - Chrome/Edge 90+
1000
- - Firefox 88+
1001
- - Safari 14+
1002
- - Opera 76+
1003
-
1004
- For older browsers, the SDK will gracefully degrade with fallback implementations.
642
+ ### Script tag vs NPM
1005
643
 
1006
- ## Performance
644
+ Choose one installation method. Using both causes conflicts.
1007
645
 
1008
- - **Bundle Size**: ~15KB minified + gzipped
1009
- - **Zero Dependencies**: No external dependencies
1010
- - **Optimized**: Smart event batching reduces network requests
1011
- - **Fast**: Deferred loading for non-critical operations
1012
- - **Memory Management**: Automatic cleanup of resources
646
+ | Feature | Script Tag | NPM |
647
+ |---------|-----------|-----|
648
+ | Bundle Size | 0 (external) | ~15KB |
649
+ | TypeScript | No | Yes |
650
+ | Access | `window.datalyr` | `import datalyr` |
651
+ | Initialize | Automatic | `datalyr.init()` |
1013
652
 
1014
- ## Privacy & Compliance
1015
-
1016
- The SDK is designed with privacy in mind:
1017
-
1018
- - **GDPR Compliant**: Built-in consent management
1019
- - **CCPA Support**: "Do Not Sell" preference support
1020
- - **Cookie Control**: Configurable cookie settings
1021
- - **Data Minimization**: Collect only what you need
1022
- - **User Rights**: Easy opt-out and data deletion
1023
-
1024
- ## Debugging
1025
-
1026
- Enable debug mode for detailed logging:
1027
-
1028
- ```javascript
1029
- import datalyr from '@datalyr/web';
1030
-
1031
- datalyr.init({
1032
- workspaceId: 'YOUR_WORKSPACE_ID',
1033
- debug: true
1034
- });
1035
-
1036
- // Check console for detailed logs:
1037
- // [Datalyr] SDK initialized
1038
- // [Datalyr] Event tracked: Button Clicked
1039
- // [Datalyr] Batch sent successfully: 5 events
1040
- ```
1041
-
1042
- ## API Reference
1043
-
1044
- ### Initialization Options
1045
-
1046
- | Option | Type | Default | Description |
1047
- |--------|------|---------|-------------|
1048
- | `workspaceId` | `string` | *required* | Your Datalyr workspace ID |
1049
- | `endpoint` | `string` | Default API | Custom endpoint URL |
1050
- | `debug` | `boolean` | `false` | Enable debug logging |
1051
- | `batchSize` | `number` | `10` | Number of events per batch |
1052
- | `flushInterval` | `number` | `5000` | Time between flushes (ms) |
1053
- | `sessionTimeout` | `number` | `1800000` | Session timeout (ms) |
1054
- | `privacyMode` | `'standard' \| 'strict'` | `'standard'` | Privacy level |
1055
- | `respectDoNotTrack` | `boolean` | `false` | Honor browser DNT |
1056
- | `respectGlobalPrivacyControl` | `boolean` | `true` | Honor GPC |
1057
- | `cookieDomain` | `string \| 'auto'` | `'auto'` | Cookie domain |
1058
- | `trackSPA` | `boolean` | `true` | Track SPA routes |
1059
- | `trackPageViews` | `boolean` | `true` | Track page views |
1060
- | `enableContainer` | `boolean` | `true` | Enable secure container script loading |
1061
-
1062
- ### Methods
1063
-
1064
- | Method | Description |
1065
- |--------|-------------|
1066
- | `init(config)` | Initialize the SDK with configuration |
1067
- | `track(event, properties?)` | Track an event |
1068
- | `identify(userId?, traits?)` | Identify a user |
1069
- | `page(name?, properties?)` | Track a page view |
1070
- | `alias(userId)` | Create an alias |
1071
- | `reset()` | Reset identity |
1072
- | `optOut()` | Opt out of tracking |
1073
- | `optIn()` | Opt back in |
1074
- | `setConsent(config)` | Set consent preferences |
1075
- | `flush()` | Force send events |
1076
- | `getAnonymousId()` | Get anonymous ID |
1077
- | `getUserId()` | Get user ID |
1078
- | `getDistinctId()` | Get distinct ID |
1079
- | `getSessionId()` | Get session ID |
1080
- | `getSessionData()` | Get session data |
1081
- | `getErrors()` | Get SDK errors |
1082
- | `loadScript(scriptId)` | Manually trigger a container script |
1083
- | `getLoadedScripts()` | Get list of loaded container scripts |
1084
- | `destroy()` | Clean up resources |
1085
-
1086
- ## Support
1087
-
1088
- - **Documentation**: [https://docs.datalyr.com](https://docs.datalyr.com)
1089
- - **Issues**: [GitHub Issues](https://github.com/datalyr/web/issues)
1090
- - **Email**: support@datalyr.com
653
+ ---
1091
654
 
1092
655
  ## License
1093
656
 
1094
- MIT License - see [LICENSE](LICENSE) file for details.
1095
-
1096
- ---
1097
-
1098
- Built by the Datalyr team
657
+ MIT