@datalyr/web 1.0.3 → 1.0.5
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 +122 -0
- package/dist/attribution.d.ts +12 -0
- package/dist/attribution.d.ts.map +1 -1
- package/dist/datalyr.cjs.js +57 -2
- package/dist/datalyr.cjs.js.map +1 -1
- package/dist/datalyr.esm.js +57 -2
- 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 +57 -2
- package/dist/datalyr.js.map +1 -1
- package/dist/datalyr.min.js +1 -1
- package/dist/datalyr.min.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -504,6 +504,128 @@ export default function RootLayout({ children }) {
|
|
|
504
504
|
}
|
|
505
505
|
```
|
|
506
506
|
|
|
507
|
+
## Authentication & Server-Side Tracking
|
|
508
|
+
|
|
509
|
+
### ⚠️ Critical: Server-Side Identification for Authentication
|
|
510
|
+
|
|
511
|
+
When implementing authentication flows (login, signup, OAuth), you **MUST** call identify on the server-side before redirecting. Client-side identification during auth flows is unreliable and will cause attribution data loss.
|
|
512
|
+
|
|
513
|
+
#### The Problem
|
|
514
|
+
|
|
515
|
+
```javascript
|
|
516
|
+
// ❌ WRONG - Client-side only (unreliable for auth flows)
|
|
517
|
+
function LoginPage() {
|
|
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) {
|
|
532
|
+
const user = await authenticate(email, password);
|
|
533
|
+
|
|
534
|
+
// Critical: Identify BEFORE redirect
|
|
535
|
+
await fetch('https://datalyr.com/api/v1/events', {
|
|
536
|
+
method: 'POST',
|
|
537
|
+
headers: {
|
|
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
|
+
})
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
redirect('/dashboard');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// For signup
|
|
556
|
+
async function signupAction(email: string, password: string) {
|
|
557
|
+
const user = await createUser(email, password);
|
|
558
|
+
|
|
559
|
+
// Identify new user server-side
|
|
560
|
+
await identifyUserServer(user.id, {
|
|
561
|
+
email: user.email,
|
|
562
|
+
created_at: new Date().toISOString(),
|
|
563
|
+
source: 'signup'
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
redirect('/onboarding');
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Why Server-Side Identification Matters
|
|
571
|
+
|
|
572
|
+
1. **Attribution Tracking**: Links anonymous visitors who clicked ads (with fbclid, gclid, etc.) to authenticated users
|
|
573
|
+
2. **Cross-Device Tracking**: Connects users across different devices and browsers
|
|
574
|
+
3. **Reliability**: Guarantees execution before redirects, unlike client-side
|
|
575
|
+
4. **OAuth Compatibility**: Works with complex OAuth redirect chains
|
|
576
|
+
|
|
577
|
+
### Implementation Checklist
|
|
578
|
+
|
|
579
|
+
Ensure you call identify server-side in ALL these scenarios:
|
|
580
|
+
|
|
581
|
+
- [ ] **Email Signup**: Call identify when creating new accounts
|
|
582
|
+
- [ ] **Email Login**: Call identify for returning users
|
|
583
|
+
- [ ] **OAuth Signup**: Call identify in OAuth callback for new users
|
|
584
|
+
- [ ] **OAuth Login**: Call identify in OAuth callback for existing users
|
|
585
|
+
- [ ] **Magic Links**: Call identify when validating magic link tokens
|
|
586
|
+
- [ ] **Password Reset**: Call identify after successful password reset
|
|
587
|
+
- [ ] **Session Restore**: Call identify when validating existing sessions on app load
|
|
588
|
+
|
|
589
|
+
### Example: Complete OAuth Implementation
|
|
590
|
+
|
|
591
|
+
```javascript
|
|
592
|
+
// app/auth/callback/route.ts
|
|
593
|
+
export async function GET(request: Request) {
|
|
594
|
+
const { user } = await validateOAuthCallback(request);
|
|
595
|
+
|
|
596
|
+
const existingUser = await getUserByEmail(user.email);
|
|
597
|
+
|
|
598
|
+
if (existingUser) {
|
|
599
|
+
// Existing user logging in
|
|
600
|
+
await identifyUserServer(existingUser.id, {
|
|
601
|
+
email: existingUser.email,
|
|
602
|
+
source: 'oauth_login',
|
|
603
|
+
provider: 'google'
|
|
604
|
+
});
|
|
605
|
+
await trackEventServer('login', { method: 'oauth' });
|
|
606
|
+
} else {
|
|
607
|
+
// New user signing up
|
|
608
|
+
const newUser = await createUser(user);
|
|
609
|
+
await identifyUserServer(newUser.id, {
|
|
610
|
+
email: newUser.email,
|
|
611
|
+
source: 'oauth_signup',
|
|
612
|
+
provider: 'google'
|
|
613
|
+
});
|
|
614
|
+
await trackEventServer('signup', { method: 'oauth' });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return redirect('/dashboard');
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### What Happens Without Server-Side Identify
|
|
622
|
+
|
|
623
|
+
Without proper server-side identification:
|
|
624
|
+
- **Lost Attribution**: Ad clicks (fbclid, gclid) won't be associated with conversions
|
|
625
|
+
- **Broken Cross-Device**: Users on multiple devices won't be linked
|
|
626
|
+
- **Incomplete Funnels**: User journeys will appear fragmented
|
|
627
|
+
- **Inaccurate Analytics**: Conversion rates and ROI calculations will be wrong
|
|
628
|
+
|
|
507
629
|
## Best Practices
|
|
508
630
|
|
|
509
631
|
### 1. Initialize Early
|
package/dist/attribution.d.ts
CHANGED
|
@@ -41,6 +41,18 @@ export declare class AttributionManager {
|
|
|
41
41
|
* Get customer journey
|
|
42
42
|
*/
|
|
43
43
|
getJourney(): TouchPoint[];
|
|
44
|
+
/**
|
|
45
|
+
* Capture advertising platform cookies
|
|
46
|
+
*/
|
|
47
|
+
private captureAdCookies;
|
|
48
|
+
/**
|
|
49
|
+
* Check if we have a specific click ID in current params
|
|
50
|
+
*/
|
|
51
|
+
private hasClickId;
|
|
52
|
+
/**
|
|
53
|
+
* Get current fbclid from URL if present
|
|
54
|
+
*/
|
|
55
|
+
private getCurrentFbclid;
|
|
44
56
|
/**
|
|
45
57
|
* Get attribution data for event
|
|
46
58
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attribution.d.ts","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEvD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,aAAa,CAAW;IAChC,OAAO,CAAC,UAAU,CAA2E;IAE7F,OAAO,CAAC,SAAS,CAgBf;IAEF,OAAO,CAAC,sBAAsB,CAO5B;gBAEU,OAAO,GAAE;QACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB;IAMN;;OAEG;IACH,kBAAkB,IAAI,WAAW;IAwDjC;;OAEG;IACH,eAAe,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAU/C;;OAEG;IACH,aAAa,IAAI,WAAW,GAAG,IAAI;IAInC;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAO9C;;OAEG;IACH,YAAY,IAAI,WAAW,GAAG,IAAI;IAIlC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI;IAqBhE;;OAEG;IACH,UAAU,IAAI,UAAU,EAAE;IAI1B;;OAEG;IACH,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"attribution.d.ts","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEvD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,aAAa,CAAW;IAChC,OAAO,CAAC,UAAU,CAA2E;IAE7F,OAAO,CAAC,SAAS,CAgBf;IAEF,OAAO,CAAC,sBAAsB,CAO5B;gBAEU,OAAO,GAAE;QACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB;IAMN;;OAEG;IACH,kBAAkB,IAAI,WAAW;IAwDjC;;OAEG;IACH,eAAe,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAU/C;;OAEG;IACH,aAAa,IAAI,WAAW,GAAG,IAAI;IAInC;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAO9C;;OAEG;IACH,YAAY,IAAI,WAAW,GAAG,IAAI;IAIlC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI;IAqBhE;;OAEG;IACH,UAAU,IAAI,UAAU,EAAE;IAI1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA8CxB;;OAEG;IACH,OAAO,CAAC,UAAU;IAKlB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;OAEG;IACH,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAsDzC;;OAEG;IACH,OAAO,CAAC,eAAe;IA6CvB;;OAEG;IACH,OAAO,CAAC,eAAe;IA2BvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAQvB;;OAEG;IACH,oBAAoB,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO;IAKvD;;OAEG;IACH,uBAAuB,IAAI,IAAI;CAYhC"}
|
package/dist/datalyr.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @datalyr/web v1.0.
|
|
2
|
+
* @datalyr/web v1.0.5
|
|
3
3
|
* Datalyr Web SDK - Modern attribution tracking for web applications
|
|
4
4
|
* (c) 2025 Datalyr Inc.
|
|
5
5
|
* Released under the MIT License
|
|
@@ -942,6 +942,59 @@ class AttributionManager {
|
|
|
942
942
|
getJourney() {
|
|
943
943
|
return storage.get('dl_journey', []);
|
|
944
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* Capture advertising platform cookies
|
|
947
|
+
*/
|
|
948
|
+
captureAdCookies() {
|
|
949
|
+
const adCookies = {};
|
|
950
|
+
// Facebook/Meta cookies
|
|
951
|
+
adCookies._fbp = cookies.get('_fbp');
|
|
952
|
+
adCookies._fbc = cookies.get('_fbc');
|
|
953
|
+
// Google Ads cookies
|
|
954
|
+
adCookies._gcl_aw = cookies.get('_gcl_aw');
|
|
955
|
+
adCookies._gcl_dc = cookies.get('_gcl_dc');
|
|
956
|
+
adCookies._gcl_gb = cookies.get('_gcl_gb');
|
|
957
|
+
adCookies._gcl_ha = cookies.get('_gcl_ha');
|
|
958
|
+
adCookies._gac = cookies.get('_gac');
|
|
959
|
+
// Google Analytics cookies
|
|
960
|
+
adCookies._ga = cookies.get('_ga');
|
|
961
|
+
adCookies._gid = cookies.get('_gid');
|
|
962
|
+
// TikTok cookies
|
|
963
|
+
adCookies._ttp = cookies.get('_ttp');
|
|
964
|
+
adCookies._ttc = cookies.get('_ttc');
|
|
965
|
+
// Generate _fbp if missing (Facebook browser ID)
|
|
966
|
+
if (!adCookies._fbp && (this.hasClickId('fbclid') || adCookies._fbc)) {
|
|
967
|
+
const timestamp = Date.now();
|
|
968
|
+
const randomId = Math.random().toString(36).substring(2, 15);
|
|
969
|
+
adCookies._fbp = `fb.1.${timestamp}.${randomId}`;
|
|
970
|
+
// Optionally set the cookie for future use
|
|
971
|
+
cookies.set('_fbp', adCookies._fbp, 90);
|
|
972
|
+
}
|
|
973
|
+
// Generate _fbc if we have fbclid but no _fbc
|
|
974
|
+
const fbclid = this.getCurrentFbclid();
|
|
975
|
+
if (fbclid && !adCookies._fbc) {
|
|
976
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
977
|
+
adCookies._fbc = `fb.1.${timestamp}.${fbclid}`;
|
|
978
|
+
// Optionally set the cookie for future use
|
|
979
|
+
cookies.set('_fbc', adCookies._fbc, 90);
|
|
980
|
+
}
|
|
981
|
+
// Filter out null values for cleaner data
|
|
982
|
+
return Object.fromEntries(Object.entries(adCookies).filter(([_, value]) => value !== null));
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Check if we have a specific click ID in current params
|
|
986
|
+
*/
|
|
987
|
+
hasClickId(clickIdType) {
|
|
988
|
+
const params = getAllQueryParams();
|
|
989
|
+
return !!params[clickIdType];
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Get current fbclid from URL if present
|
|
993
|
+
*/
|
|
994
|
+
getCurrentFbclid() {
|
|
995
|
+
const params = getAllQueryParams();
|
|
996
|
+
return params.fbclid || null;
|
|
997
|
+
}
|
|
945
998
|
/**
|
|
946
999
|
* Get attribution data for event
|
|
947
1000
|
*/
|
|
@@ -950,6 +1003,8 @@ class AttributionManager {
|
|
|
950
1003
|
const lastTouch = this.getLastTouch();
|
|
951
1004
|
const journey = this.getJourney();
|
|
952
1005
|
const current = this.captureAttribution();
|
|
1006
|
+
// Capture advertising cookies automatically
|
|
1007
|
+
const adCookies = this.captureAdCookies();
|
|
953
1008
|
// Update first/last touch if needed
|
|
954
1009
|
if (!firstTouch && Object.keys(current).length > 1) {
|
|
955
1010
|
this.storeFirstTouch(current);
|
|
@@ -957,7 +1012,7 @@ class AttributionManager {
|
|
|
957
1012
|
if (Object.keys(current).length > 1) {
|
|
958
1013
|
this.storeLastTouch(current);
|
|
959
1014
|
}
|
|
960
|
-
return Object.assign(Object.assign({}, current), {
|
|
1015
|
+
return Object.assign(Object.assign(Object.assign({}, current), adCookies), {
|
|
961
1016
|
// First touch (with snake_case aliases)
|
|
962
1017
|
first_touch_source: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.source, first_touch_medium: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.medium, first_touch_campaign: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.campaign, first_touch_timestamp: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.timestamp, firstTouchSource: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.source, firstTouchMedium: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.medium, firstTouchCampaign: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.campaign,
|
|
963
1018
|
// Last touch (with snake_case aliases)
|