@datalyr/web 1.0.5 → 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 +143 -94
- 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,127 +504,171 @@ export default function RootLayout({ children }) {
|
|
|
504
504
|
}
|
|
505
505
|
```
|
|
506
506
|
|
|
507
|
-
## Authentication &
|
|
507
|
+
## Authentication & Attribution Tracking
|
|
508
508
|
|
|
509
|
-
### ⚠️ Critical:
|
|
509
|
+
### ⚠️ Critical: Client-Side Identification for Attribution
|
|
510
510
|
|
|
511
|
-
When implementing authentication flows (login, signup, OAuth), you **MUST** call identify on the
|
|
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
512
|
|
|
513
|
-
#### The Problem
|
|
513
|
+
#### The Problem with Server-Side Identify
|
|
514
514
|
|
|
515
515
|
```javascript
|
|
516
|
-
// ❌ WRONG -
|
|
517
|
-
|
|
518
|
-
const handleLogin = async () => {
|
|
519
|
-
await login(email, password);
|
|
520
|
-
datalyr.identify(userId); // May not execute before redirect!
|
|
521
|
-
router.push('/dashboard'); // User redirects, identify never completes
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
#### The Solution
|
|
527
|
-
|
|
528
|
-
```javascript
|
|
529
|
-
// ✅ CORRECT - Server-side identification
|
|
530
|
-
// Next.js Server Action example
|
|
531
|
-
async function loginAction(email: string, password: string) {
|
|
516
|
+
// ❌ WRONG - Server-side identify breaks attribution
|
|
517
|
+
app.post('/api/login', async (req, res) => {
|
|
532
518
|
const user = await authenticate(email, password);
|
|
533
519
|
|
|
534
|
-
//
|
|
535
|
-
await
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
'Authorization': `Bearer ${process.env.DATALYR_API_KEY}`,
|
|
539
|
-
'Content-Type': 'application/json'
|
|
540
|
-
},
|
|
541
|
-
body: JSON.stringify({
|
|
542
|
-
event_name: '$identify',
|
|
543
|
-
user_id: user.id,
|
|
544
|
-
workspace_id: process.env.DATALYR_WORKSPACE_ID,
|
|
545
|
-
properties: {
|
|
546
|
-
email: user.email,
|
|
547
|
-
source: 'login'
|
|
548
|
-
}
|
|
549
|
-
})
|
|
520
|
+
// This creates a NEW session without attribution data!
|
|
521
|
+
await serverAnalytics.identify({
|
|
522
|
+
userId: user.id,
|
|
523
|
+
traits: { email: user.email }
|
|
550
524
|
});
|
|
551
525
|
|
|
552
|
-
|
|
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
|
+
};
|
|
553
556
|
}
|
|
554
557
|
|
|
555
|
-
//
|
|
556
|
-
async
|
|
557
|
-
const user = await
|
|
558
|
+
// Server endpoint - NO identify, just auth
|
|
559
|
+
app.post('/api/login', async (req, res) => {
|
|
560
|
+
const user = await authenticate(email, password);
|
|
558
561
|
|
|
559
|
-
//
|
|
560
|
-
|
|
562
|
+
// Just return user data for client-side identify
|
|
563
|
+
res.json({
|
|
564
|
+
userId: user.id,
|
|
561
565
|
email: user.email,
|
|
562
|
-
|
|
563
|
-
source: 'signup'
|
|
566
|
+
name: user.name
|
|
564
567
|
});
|
|
565
|
-
|
|
566
|
-
redirect('/onboarding');
|
|
567
|
-
}
|
|
568
|
+
});
|
|
568
569
|
```
|
|
569
570
|
|
|
570
|
-
### Why
|
|
571
|
+
### Why Client-Side Identification Matters
|
|
571
572
|
|
|
572
|
-
1. **Attribution
|
|
573
|
-
2. **
|
|
574
|
-
3. **
|
|
575
|
-
4. **
|
|
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
|
|
576
577
|
|
|
577
578
|
### Implementation Checklist
|
|
578
579
|
|
|
579
|
-
Ensure you call identify
|
|
580
|
+
Ensure you call identify **client-side** in these scenarios:
|
|
580
581
|
|
|
581
|
-
- [ ] **
|
|
582
|
-
- [ ] **
|
|
583
|
-
- [ ] **OAuth
|
|
584
|
-
- [ ] **
|
|
585
|
-
- [ ] **
|
|
586
|
-
- [ ] **
|
|
587
|
-
- [ ] **Session Restore**:
|
|
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
|
|
588
589
|
|
|
589
590
|
### Example: Complete OAuth Implementation
|
|
590
591
|
|
|
591
592
|
```javascript
|
|
592
|
-
//
|
|
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!
|
|
593
620
|
export async function GET(request: Request) {
|
|
594
621
|
const { user } = await validateOAuthCallback(request);
|
|
595
622
|
|
|
596
623
|
const existingUser = await getUserByEmail(user.email);
|
|
597
624
|
|
|
598
625
|
if (existingUser) {
|
|
599
|
-
//
|
|
600
|
-
await
|
|
601
|
-
|
|
602
|
-
source: 'oauth_login',
|
|
603
|
-
provider: 'google'
|
|
604
|
-
});
|
|
605
|
-
await trackEventServer('login', { method: 'oauth' });
|
|
626
|
+
// Set session, but DON'T identify
|
|
627
|
+
await createSession(existingUser.id);
|
|
628
|
+
return redirect('/dashboard?oauth=success');
|
|
606
629
|
} else {
|
|
607
|
-
//
|
|
630
|
+
// Create user, but DON'T identify
|
|
608
631
|
const newUser = await createUser(user);
|
|
609
|
-
await
|
|
610
|
-
|
|
611
|
-
source: 'oauth_signup',
|
|
612
|
-
provider: 'google'
|
|
613
|
-
});
|
|
614
|
-
await trackEventServer('signup', { method: 'oauth' });
|
|
632
|
+
await createSession(newUser.id);
|
|
633
|
+
return redirect('/dashboard?oauth=success&new=true');
|
|
615
634
|
}
|
|
616
|
-
|
|
617
|
-
return redirect('/dashboard');
|
|
618
635
|
}
|
|
619
636
|
```
|
|
620
637
|
|
|
621
|
-
###
|
|
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!)
|
|
622
666
|
|
|
623
|
-
|
|
624
|
-
- **Lost Attribution**:
|
|
625
|
-
- **Broken
|
|
626
|
-
- **
|
|
627
|
-
- **
|
|
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
|
|
628
672
|
|
|
629
673
|
## Best Practices
|
|
630
674
|
|
|
@@ -679,15 +723,20 @@ datalyr.track('Product Viewed', {
|
|
|
679
723
|
});
|
|
680
724
|
```
|
|
681
725
|
|
|
682
|
-
### 4. Identify
|
|
683
|
-
|
|
726
|
+
### 4. ALWAYS Identify in the Browser (Not Server)
|
|
727
|
+
This is critical for attribution:
|
|
684
728
|
|
|
685
729
|
```javascript
|
|
686
|
-
//
|
|
730
|
+
// ✅ CORRECT - Browser identification
|
|
687
731
|
async function handleLogin(email, password) {
|
|
688
|
-
const
|
|
732
|
+
const response = await fetch('/api/login', {
|
|
733
|
+
method: 'POST',
|
|
734
|
+
body: JSON.stringify({ email, password })
|
|
735
|
+
});
|
|
689
736
|
|
|
690
|
-
|
|
737
|
+
const user = await response.json();
|
|
738
|
+
|
|
739
|
+
// Identify in browser - preserves attribution!
|
|
691
740
|
datalyr.identify(user.id, {
|
|
692
741
|
email: user.email,
|
|
693
742
|
name: user.name,
|
|
@@ -695,13 +744,13 @@ async function handleLogin(email, password) {
|
|
|
695
744
|
});
|
|
696
745
|
}
|
|
697
746
|
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
747
|
+
// ❌ WRONG - Server identification
|
|
748
|
+
// This breaks attribution tracking!
|
|
749
|
+
app.post('/api/login', async (req, res) => {
|
|
750
|
+
const user = await authenticate(req.body);
|
|
701
751
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
name: data.name,
|
|
752
|
+
// DON'T DO THIS - Creates new session
|
|
753
|
+
await serverAnalytics.identify(user.id, {
|
|
705
754
|
signup_date: new Date().toISOString()
|
|
706
755
|
});
|
|
707
756
|
|
package/dist/datalyr.cjs.js
CHANGED
package/dist/datalyr.esm.js
CHANGED
package/dist/datalyr.js
CHANGED