@datalyr/web 1.0.4 → 1.0.6
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 +182 -11
- package/dist/datalyr.cjs.js +1 -1
- package/dist/datalyr.esm.js +1 -1
- package/dist/datalyr.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -504,6 +504,172 @@ export default function RootLayout({ children }) {
|
|
|
504
504
|
}
|
|
505
505
|
```
|
|
506
506
|
|
|
507
|
+
## Authentication & Attribution Tracking
|
|
508
|
+
|
|
509
|
+
### ⚠️ Critical: Client-Side Identification for Attribution
|
|
510
|
+
|
|
511
|
+
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.
|
|
512
|
+
|
|
513
|
+
#### The Problem with Server-Side Identify
|
|
514
|
+
|
|
515
|
+
```javascript
|
|
516
|
+
// ❌ WRONG - Server-side identify breaks attribution
|
|
517
|
+
app.post('/api/login', async (req, res) => {
|
|
518
|
+
const user = await authenticate(email, password);
|
|
519
|
+
|
|
520
|
+
// This creates a NEW session without attribution data!
|
|
521
|
+
await serverAnalytics.identify({
|
|
522
|
+
userId: user.id,
|
|
523
|
+
traits: { email: user.email }
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
res.json({ userId: user.id });
|
|
527
|
+
});
|
|
528
|
+
// Result: Lost UTM params, fbclid, gclid, referrer - all gone!
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
#### The Correct Solution
|
|
532
|
+
|
|
533
|
+
```javascript
|
|
534
|
+
// ✅ CORRECT - Client-side identification preserves attribution
|
|
535
|
+
// React/Next.js example
|
|
536
|
+
function LoginPage() {
|
|
537
|
+
const handleLogin = async () => {
|
|
538
|
+
// 1. Authenticate on server
|
|
539
|
+
const response = await fetch('/api/login', {
|
|
540
|
+
method: 'POST',
|
|
541
|
+
body: JSON.stringify({ email, password })
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const { userId, email, name } = await response.json();
|
|
545
|
+
|
|
546
|
+
// 2. Identify in browser - this preserves attribution!
|
|
547
|
+
datalyr.identify(userId, {
|
|
548
|
+
email: email,
|
|
549
|
+
name: name,
|
|
550
|
+
login_method: 'email'
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// 3. Then redirect
|
|
554
|
+
router.push('/dashboard');
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Server endpoint - NO identify, just auth
|
|
559
|
+
app.post('/api/login', async (req, res) => {
|
|
560
|
+
const user = await authenticate(email, password);
|
|
561
|
+
|
|
562
|
+
// Just return user data for client-side identify
|
|
563
|
+
res.json({
|
|
564
|
+
userId: user.id,
|
|
565
|
+
email: user.email,
|
|
566
|
+
name: user.name
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Why Client-Side Identification Matters
|
|
572
|
+
|
|
573
|
+
1. **Attribution Preserved**: UTM params, fbclid, gclid, referrer all stay linked to the user
|
|
574
|
+
2. **Session Continuity**: Links anonymous session (with all its context) to identified user
|
|
575
|
+
3. **Cookie Access**: Can read and maintain browser session data
|
|
576
|
+
4. **Accurate Journey**: Tracks the complete anonymous → identified user journey
|
|
577
|
+
|
|
578
|
+
### Implementation Checklist
|
|
579
|
+
|
|
580
|
+
Ensure you call identify **client-side** in these scenarios:
|
|
581
|
+
|
|
582
|
+
- [ ] **After Signup Success**: When account creation API returns success
|
|
583
|
+
- [ ] **After Login Success**: When authentication API returns success
|
|
584
|
+
- [ ] **After OAuth Redirect**: When returning from OAuth provider
|
|
585
|
+
- [ ] **After Magic Link Click**: When magic link is validated
|
|
586
|
+
- [ ] **On App Load**: If user session exists, re-identify
|
|
587
|
+
- [ ] **After Email Verification**: When user verifies their email
|
|
588
|
+
- [ ] **Session Restore**: When detecting existing authenticated session
|
|
589
|
+
|
|
590
|
+
### Example: Complete OAuth Implementation
|
|
591
|
+
|
|
592
|
+
```javascript
|
|
593
|
+
// Client-side: After OAuth redirect
|
|
594
|
+
useEffect(() => {
|
|
595
|
+
// Check if we just came back from OAuth
|
|
596
|
+
if (window.location.search.includes('oauth=success')) {
|
|
597
|
+
// Get user data from your API/session
|
|
598
|
+
fetch('/api/me')
|
|
599
|
+
.then(res => res.json())
|
|
600
|
+
.then(user => {
|
|
601
|
+
// Identify in browser - preserves attribution!
|
|
602
|
+
datalyr.identify(user.id, {
|
|
603
|
+
email: user.email,
|
|
604
|
+
name: user.name,
|
|
605
|
+
auth_method: 'google',
|
|
606
|
+
signup_source: localStorage.getItem('signup_source') || 'organic'
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Track the appropriate event
|
|
610
|
+
if (user.isNewUser) {
|
|
611
|
+
datalyr.track('Signup Completed', { method: 'oauth' });
|
|
612
|
+
} else {
|
|
613
|
+
datalyr.track('Login Successful', { method: 'oauth' });
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}, []);
|
|
618
|
+
|
|
619
|
+
// Server-side: OAuth callback - NO identify!
|
|
620
|
+
export async function GET(request: Request) {
|
|
621
|
+
const { user } = await validateOAuthCallback(request);
|
|
622
|
+
|
|
623
|
+
const existingUser = await getUserByEmail(user.email);
|
|
624
|
+
|
|
625
|
+
if (existingUser) {
|
|
626
|
+
// Set session, but DON'T identify
|
|
627
|
+
await createSession(existingUser.id);
|
|
628
|
+
return redirect('/dashboard?oauth=success');
|
|
629
|
+
} else {
|
|
630
|
+
// Create user, but DON'T identify
|
|
631
|
+
const newUser = await createUser(user);
|
|
632
|
+
await createSession(newUser.id);
|
|
633
|
+
return redirect('/dashboard?oauth=success&new=true');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### Server-Side Events (Different from Identify!)
|
|
639
|
+
|
|
640
|
+
Use server-side for **tracking events** with userId (not identify):
|
|
641
|
+
|
|
642
|
+
```javascript
|
|
643
|
+
// ✅ CORRECT - Server-side events with userId
|
|
644
|
+
// Stripe webhook example
|
|
645
|
+
app.post('/webhook/stripe', async (req, res) => {
|
|
646
|
+
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
|
|
647
|
+
|
|
648
|
+
if (event.type === 'checkout.session.completed') {
|
|
649
|
+
const session = event.data.object;
|
|
650
|
+
|
|
651
|
+
// Track purchase event with userId (NOT identify)
|
|
652
|
+
await analytics.track({
|
|
653
|
+
userId: session.client_reference_id, // Your user ID
|
|
654
|
+
event: 'Purchase Completed',
|
|
655
|
+
properties: {
|
|
656
|
+
amount: session.amount_total / 100,
|
|
657
|
+
currency: session.currency,
|
|
658
|
+
plan: session.metadata.plan
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### What Happens with Server-Side Identify (Wrong!)
|
|
666
|
+
|
|
667
|
+
If you identify server-side:
|
|
668
|
+
- **Lost Attribution**: New session created, no UTM params or click IDs
|
|
669
|
+
- **Broken Journey**: Can't link anonymous browsing to identified user
|
|
670
|
+
- **Multiple Sessions**: Each server identify creates disconnected session
|
|
671
|
+
- **No Context**: Missing referrer, landing page, device info
|
|
672
|
+
|
|
507
673
|
## Best Practices
|
|
508
674
|
|
|
509
675
|
### 1. Initialize Early
|
|
@@ -557,15 +723,20 @@ datalyr.track('Product Viewed', {
|
|
|
557
723
|
});
|
|
558
724
|
```
|
|
559
725
|
|
|
560
|
-
### 4. Identify
|
|
561
|
-
|
|
726
|
+
### 4. ALWAYS Identify in the Browser (Not Server)
|
|
727
|
+
This is critical for attribution:
|
|
562
728
|
|
|
563
729
|
```javascript
|
|
564
|
-
//
|
|
730
|
+
// ✅ CORRECT - Browser identification
|
|
565
731
|
async function handleLogin(email, password) {
|
|
566
|
-
const
|
|
732
|
+
const response = await fetch('/api/login', {
|
|
733
|
+
method: 'POST',
|
|
734
|
+
body: JSON.stringify({ email, password })
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const user = await response.json();
|
|
567
738
|
|
|
568
|
-
// Identify
|
|
739
|
+
// Identify in browser - preserves attribution!
|
|
569
740
|
datalyr.identify(user.id, {
|
|
570
741
|
email: user.email,
|
|
571
742
|
name: user.name,
|
|
@@ -573,13 +744,13 @@ async function handleLogin(email, password) {
|
|
|
573
744
|
});
|
|
574
745
|
}
|
|
575
746
|
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
747
|
+
// ❌ WRONG - Server identification
|
|
748
|
+
// This breaks attribution tracking!
|
|
749
|
+
app.post('/api/login', async (req, res) => {
|
|
750
|
+
const user = await authenticate(req.body);
|
|
579
751
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
name: data.name,
|
|
752
|
+
// DON'T DO THIS - Creates new session
|
|
753
|
+
await serverAnalytics.identify(user.id, {
|
|
583
754
|
signup_date: new Date().toISOString()
|
|
584
755
|
});
|
|
585
756
|
|
package/dist/datalyr.cjs.js
CHANGED
package/dist/datalyr.esm.js
CHANGED
package/dist/datalyr.js
CHANGED