ultimate-jekyll-manager 0.0.198 → 0.0.200
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/.claude/settings.local.json +3 -2
- package/README.md +0 -2
- package/TODO-ATTRIBUTION2.md +753 -0
- package/bin/ultimate-jekyll +1 -1
- package/dist/assets/js/core/auth.js +37 -32
- package/dist/assets/js/core/exit-popup.js +3 -0
- package/dist/assets/js/core/query-strings.js +16 -12
- package/dist/assets/js/libs/auth.js +12 -0
- package/dist/assets/js/libs/form-manager.js +1 -1
- package/dist/assets/js/pages/account/sections/api-keys.js +1 -4
- package/dist/assets/js/pages/account/sections/connections.js +11 -17
- package/dist/assets/js/pages/account/sections/delete.js +5 -15
- package/dist/assets/js/pages/account/sections/security.js +2 -6
- package/dist/assets/js/pages/admin/notifications/new/index.js +5 -16
- package/dist/assets/js/pages/download/index.js +3 -6
- package/dist/assets/js/pages/oauth2/index.js +5 -5
- package/dist/assets/js/pages/payment/checkout/modules/session.js +1 -1
- package/dist/assets/js/pages/token/index.js +1 -5
- package/dist/assets/themes/bootstrap/overrides/_avatars.scss +5 -1
- package/dist/assets/themes/bootstrap/overrides/_buttons-adaptive.scss +68 -55
- package/dist/assets/themes/bootstrap/overrides/_color-shades.scss +25 -0
- package/dist/assets/themes/bootstrap/overrides/_index.scss +1 -0
- package/dist/assets/themes/classy/css/components/_buttons.scss +1 -2
- package/dist/build.js +1 -1
- package/dist/defaults/dist/pages/test/libraries/bootstrap.html +7 -0
- package/dist/defaults/src/_config.yml +10 -6
- package/dist/gulp/tasks/defaults.js +1 -1
- package/dist/gulp/tasks/sass.js +4 -0
- package/package.json +13 -13
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
THIS IS MY SAVED CHAT FROM BEFORE OUR TRIP TO MEXICO Jan 9 2026
|
|
2
|
+
|
|
3
|
+
i need help designing a kickass attribution system. currently when the page loads we save query strings like affiliate refferral and utm tags (/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/core/query-strings.js). i want to save a few more things and do a few more things to add on to this
|
|
4
|
+
|
|
5
|
+
1. i want a custom "itm_" tag system nearly identical to utm but used for when we send users internally to track which mechanism they used such as an exit poup (look at itm tags here /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/core/exit-popup.js)
|
|
6
|
+
2. obviously in this file /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/core/query-strings.js we save the stuff locally. however thres a few times we process it ont he server side and im not sure if these are good ideas to keep, edit or remove. for example, /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/core/auth.js we send attribution to the signup handler fn but the main purpose of this is to give the referring user credit, we dont currently do anyting serverside with utm attribution. next, at checkout /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/pages/payment/checkout/modules/session.js we process the utm tags too and here we specifically filter them to be included only if they are fresher than 30 days
|
|
7
|
+
3. i want to keep track of ad shit so we can attribute ads priperly such as
|
|
8
|
+
fbclid: params.get('fbclid'),
|
|
9
|
+
fbc: getCookie('_fbc'),
|
|
10
|
+
gclid: params.get('gclid'),
|
|
11
|
+
ttclid: params.get('ttclid'),
|
|
12
|
+
|
|
13
|
+
so i hjave a few questions
|
|
14
|
+
should we maybe move the 30 day processing thing to a centralized location? so that whenever we need to process the tags they are ready to go? we could process them on query-string.js
|
|
15
|
+
how and when should we sync these with the server? liek i said we do it at signup and we also send it during checkout.
|
|
16
|
+
how do we deal with "first touch"? should we implement that too? how do we dael with it synced with server? do we sync? or just localstorage?
|
|
17
|
+
|
|
18
|
+
i want an all in one kickass system that makes it easy for us to keep track of how users got to our site so we can do utm tracking, and ad attribution conversion events.
|
|
19
|
+
|
|
20
|
+
soem events are tracked clientside, and some are tracked serverside. for example, i will be moving the purchase conversion events for GA, meta, and tiktok to server because some purchases happen offline for hte user, sucha s recurring purchases. so the attribution DOES need to be stored.
|
|
21
|
+
|
|
22
|
+
so that brings up another question, we could store attribution on the user or on their subscription document OR both. for example we could continuously update the user attribution and then the attribution object sved on the subscirptuon is the one that is sent every time a recurring purchase happens so that it attrbted to the right campaign.
|
|
23
|
+
Show less
|
|
24
|
+
I'll help you design a comprehensive attribution system. Let me start by exploring the relevant files to understand the current implementation.
|
|
25
|
+
|
|
26
|
+
Task:Explore attribution system
|
|
27
|
+
IN
|
|
28
|
+
Explore the attribution and tracking system in this Ultimate Jekyll Manager project. I need to understand:
|
|
29
|
+
|
|
30
|
+
1. How query strings are currently saved in `/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/core/query-strings.js`
|
|
31
|
+
2. How ITM tags are used in `/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/core/exit-popup.js`
|
|
32
|
+
3. How attribution is sent during auth in `/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/core/auth.js`
|
|
33
|
+
4. How UTM tags are processed at checkout in `/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/pages/payment/checkout/modules/session.js`
|
|
34
|
+
|
|
35
|
+
Focus on:
|
|
36
|
+
- What data is currently being saved
|
|
37
|
+
- How it's stored (localStorage keys, structure)
|
|
38
|
+
- When/how it's sent to the server
|
|
39
|
+
- Any existing timestamp/freshness logic
|
|
40
|
+
|
|
41
|
+
Read all 4 files thoroughly and provide detailed findings.
|
|
42
|
+
Now let me also check if there's a TODO-tracking.md file and any existing ad click ID handling:
|
|
43
|
+
|
|
44
|
+
Read TODO-tracking.md
|
|
45
|
+
Grep "fbclid|gclid|ttclid|fbc|_fbc" (in /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src)
|
|
46
|
+
1 line of output
|
|
47
|
+
Now let me check how the backend handles attribution to understand the full picture:
|
|
48
|
+
|
|
49
|
+
Grep "user:sign-up|attribution" (in /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src)
|
|
50
|
+
4 lines of output
|
|
51
|
+
Let me also check what webManager storage utilities look like, and understand the broader context of how this will interact with backend-manager:
|
|
52
|
+
|
|
53
|
+
Grep "getContext|utilities\(\)" (in /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js)
|
|
54
|
+
28 lines of output
|
|
55
|
+
I now have a comprehensive understanding. Let me ask some clarifying questions before designing the system:
|
|
56
|
+
|
|
57
|
+
AskUserQuestion
|
|
58
|
+
OUT
|
|
59
|
+
User has answered your questions: "For first-touch vs last-touch attribution, which approach do you want for EACH data type?"="what is better? could we do something like first touch and dont overwrite for 30 days? or is that bad? ", "When should attribution be synced to the server/Firestore?"="Signup + Checkout only", "Where should attribution be stored in Firestore?"="User + Subscription (Recommended)", "Should the 30-day freshness window apply globally or be configurable per data type?"="Global 30 days (Recommended)". You can now continue with the user's answers in mind.
|
|
60
|
+
Great questions! Let me address the first-touch model and then launch a Plan agent to design this system properly.
|
|
61
|
+
|
|
62
|
+
On "First-touch but don't overwrite for 30 days":
|
|
63
|
+
|
|
64
|
+
This is actually a really smart hybrid approach! Here's why it works well:
|
|
65
|
+
|
|
66
|
+
First-touch priority: The initial touchpoint (how they discovered you) is often the most valuable for understanding channel effectiveness
|
|
67
|
+
30-day refresh window: After 30 days, if they come back via a NEW campaign, they're essentially a "re-acquired" user - their journey has reset
|
|
68
|
+
Simpler than tracking both: Less storage, less complexity, clear rules
|
|
69
|
+
The logic would be:
|
|
70
|
+
|
|
71
|
+
If no attribution exists → save it (first touch)
|
|
72
|
+
If attribution exists AND is < 30 days old → don't overwrite (preserve first touch)
|
|
73
|
+
If attribution exists AND is > 30 days old → overwrite (new journey)
|
|
74
|
+
This is arguably better than pure "first + last" because it respects the reality that user journeys have a shelf life.
|
|
75
|
+
|
|
76
|
+
Let me now design the comprehensive system:
|
|
77
|
+
|
|
78
|
+
Task:Design attribution system
|
|
79
|
+
IN
|
|
80
|
+
Design a comprehensive attribution tracking system for Ultimate Jekyll Manager. This system needs to track how users discover and interact with the site for analytics and ad conversion attribution.
|
|
81
|
+
|
|
82
|
+
## CURRENT STATE
|
|
83
|
+
|
|
84
|
+
### Files to Modify:
|
|
85
|
+
1. `src/assets/js/core/query-strings.js` - Currently captures UTM tags and affiliate codes
|
|
86
|
+
2. `src/assets/js/core/auth.js` - Sends attribution on signup
|
|
87
|
+
3. `src/assets/js/pages/payment/checkout/modules/session.js` - Uses 30-day UTM freshness check
|
|
88
|
+
|
|
89
|
+
### Current Storage Structure:
|
|
90
|
+
```javascript
|
|
91
|
+
// localStorage key: "attribution"
|
|
92
|
+
{
|
|
93
|
+
affiliate: { code, timestamp, url, page },
|
|
94
|
+
utm: { tags: { utm_source, utm_medium, utm_campaign, utm_term, utm_content }, timestamp, url, page }
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Current Issues:
|
|
99
|
+
- No ITM (internal tracking) tag capture
|
|
100
|
+
- No ad click ID capture (fbclid, gclid, ttclid, fbc cookie)
|
|
101
|
+
- 30-day freshness logic only at checkout, not centralized
|
|
102
|
+
- No first-touch protection
|
|
103
|
+
|
|
104
|
+
## REQUIREMENTS
|
|
105
|
+
|
|
106
|
+
### 1. Data to Capture
|
|
107
|
+
- **UTM tags**: utm_source, utm_medium, utm_campaign, utm_term, utm_content
|
|
108
|
+
- **ITM tags** (new): itm_source, itm_medium, itm_campaign, itm_content (internal tracking, similar to UTM)
|
|
109
|
+
- **Affiliate**: aff or ref query param
|
|
110
|
+
- **Ad Click IDs** (new):
|
|
111
|
+
- fbclid (Facebook/Meta click ID from URL)
|
|
112
|
+
- fbc (Facebook cookie `_fbc`)
|
|
113
|
+
- gclid (Google Ads click ID)
|
|
114
|
+
- ttclid (TikTok click ID)
|
|
115
|
+
|
|
116
|
+
### 2. Attribution Model: "First-Touch with 30-Day Refresh"
|
|
117
|
+
- If no attribution exists → save it
|
|
118
|
+
- If attribution exists AND < 30 days old → DON'T overwrite (preserve first touch)
|
|
119
|
+
- If attribution exists AND > 30 days old → overwrite (new journey)
|
|
120
|
+
- Apply this logic independently to each category (UTM, ITM, affiliate, ad_clicks)
|
|
121
|
+
|
|
122
|
+
### 3. Centralized Freshness Processing
|
|
123
|
+
Create a helper method that returns "fresh" attribution (< 30 days). This replaces the ad-hoc 30-day check currently in checkout session.js.
|
|
124
|
+
|
|
125
|
+
### 4. Server Sync Points
|
|
126
|
+
- **Signup**: Send full attribution to backend (current behavior, enhance with new data)
|
|
127
|
+
- **Checkout**: Include fresh attribution in payment intent (current behavior, enhance with new data)
|
|
128
|
+
- Backend stores on user doc (rolling) and subscription doc (snapshot at purchase)
|
|
129
|
+
|
|
130
|
+
### 5. LocalStorage Structure (New)
|
|
131
|
+
Design a clean structure that supports all data types with timestamps.
|
|
132
|
+
|
|
133
|
+
## DESIGN CONSTRAINTS
|
|
134
|
+
|
|
135
|
+
- Use webManager.storage() for localStorage access
|
|
136
|
+
- Follow existing code patterns in the codebase
|
|
137
|
+
- ITM tags should mirror UTM structure
|
|
138
|
+
- Ad click IDs should be grouped together
|
|
139
|
+
- All timestamps should be ISO strings
|
|
140
|
+
- Cookies accessed via standard document.cookie parsing
|
|
141
|
+
|
|
142
|
+
## DELIVERABLES
|
|
143
|
+
|
|
144
|
+
1. New storage structure design
|
|
145
|
+
2. Refactored query-strings.js with:
|
|
146
|
+
- ITM capture
|
|
147
|
+
- Ad click ID capture
|
|
148
|
+
- First-touch with 30-day refresh logic
|
|
149
|
+
- Centralized `getFreshAttribution()` helper
|
|
150
|
+
3. Updates to auth.js to send enhanced attribution
|
|
151
|
+
4. Updates to checkout session.js to use centralized helper
|
|
152
|
+
5. Helper function to read _fbc cookie
|
|
153
|
+
|
|
154
|
+
Provide a detailed implementation plan with code structure.
|
|
155
|
+
Excellent! The Plan agent has designed a comprehensive system. Let me now review the critical files and ask a couple more clarifying questions before finalizing the plan.
|
|
156
|
+
|
|
157
|
+
Read query-strings.js, exit-popup.js
|
|
158
|
+
Read auth.js
|
|
159
|
+
Read session.js
|
|
160
|
+
Now I have all the context I need. Let me ask one final clarifying question before writing the plan:
|
|
161
|
+
|
|
162
|
+
AskUserQuestion
|
|
163
|
+
OUT
|
|
164
|
+
User has answered your questions: "Should ITM tags have different freshness behavior than UTM tags? (e.g., ITM is 'internal' so maybe a shorter window makes sense)"="Same as UTM (30 days) (Recommended)". You can now continue with the user's answers in mind.
|
|
165
|
+
Now I have all the information I need. Let me write the comprehensive plan:
|
|
166
|
+
|
|
167
|
+
Write woolly-stargazing-dove.md
|
|
168
|
+
246 lines
|
|
169
|
+
I've written the comprehensive plan. Here's a summary of the attribution system design:
|
|
170
|
+
|
|
171
|
+
Summary
|
|
172
|
+
Attribution Model: First-touch with 30-day refresh
|
|
173
|
+
|
|
174
|
+
If no data exists → save it
|
|
175
|
+
If data < 30 days old → preserve (don't overwrite)
|
|
176
|
+
If data >= 30 days old → overwrite (new journey)
|
|
177
|
+
Data Captured:
|
|
178
|
+
|
|
179
|
+
UTM: External marketing (Google, Facebook ads, etc.)
|
|
180
|
+
ITM: Internal tracking (exit popups, cross-property journeys)
|
|
181
|
+
Affiliate: ?aff= or ?ref= codes
|
|
182
|
+
Ad Clicks: fbclid, fbc (cookie), gclid, ttclid
|
|
183
|
+
Files to Modify:
|
|
184
|
+
|
|
185
|
+
query-strings.js - Major refactor with ITM, ad clicks, first-touch logic, and centralized getFresh() API
|
|
186
|
+
auth.js - Simple change to use webManager.uj().attribution.getFresh()
|
|
187
|
+
session.js - Remove ad-hoc 30-day check, expand payment intent with full attribution
|
|
188
|
+
Key Benefits:
|
|
189
|
+
|
|
190
|
+
Centralized freshness logic (no more scattered 30-day checks)
|
|
191
|
+
Clean public API: webManager.uj().attribution.getFresh()
|
|
192
|
+
Proper ad platform click ID capture for server-side conversion events
|
|
193
|
+
ITM enables tracking internal mechanisms (exit popup, extension prompts, etc.)
|
|
194
|
+
Does this plan look good to proceed with implementation?
|
|
195
|
+
|
|
196
|
+
Claude’s Plan
|
|
197
|
+
Attribution Tracking System - Implementation Plan
|
|
198
|
+
Overview
|
|
199
|
+
Build a comprehensive attribution tracking system that captures UTM tags, ITM tags (internal tracking), affiliate codes, and ad click IDs with a "first-touch with 30-day refresh" model.
|
|
200
|
+
|
|
201
|
+
Design Decisions
|
|
202
|
+
Attribution Model: First-touch with 30-day refresh (don't overwrite fresh data, allow overwrite after 30 days)
|
|
203
|
+
Freshness TTL: 30 days globally for all attribution types (UTM, ITM, affiliate, adClicks)
|
|
204
|
+
Server Sync: Signup + Checkout only (current behavior, enhanced with new data)
|
|
205
|
+
Storage: User doc (rolling latest) + Subscription doc (snapshot at purchase)
|
|
206
|
+
New Storage Structure
|
|
207
|
+
|
|
208
|
+
// localStorage key: "attribution"
|
|
209
|
+
{
|
|
210
|
+
utm: {
|
|
211
|
+
tags: { utm_source, utm_medium, utm_campaign, utm_term, utm_content },
|
|
212
|
+
timestamp: "ISO string",
|
|
213
|
+
url: "full landing URL",
|
|
214
|
+
page: "/path"
|
|
215
|
+
},
|
|
216
|
+
itm: {
|
|
217
|
+
tags: { itm_source, itm_medium, itm_campaign, itm_content },
|
|
218
|
+
timestamp: "ISO string",
|
|
219
|
+
url: "full URL",
|
|
220
|
+
page: "/path"
|
|
221
|
+
},
|
|
222
|
+
affiliate: {
|
|
223
|
+
code: "partner123",
|
|
224
|
+
timestamp: "ISO string",
|
|
225
|
+
url: "full URL",
|
|
226
|
+
page: "/path"
|
|
227
|
+
},
|
|
228
|
+
adClicks: {
|
|
229
|
+
fbclid: "from URL param",
|
|
230
|
+
fbc: "from _fbc cookie",
|
|
231
|
+
gclid: "from URL param",
|
|
232
|
+
ttclid: "from URL param",
|
|
233
|
+
timestamp: "ISO string",
|
|
234
|
+
url: "full URL",
|
|
235
|
+
page: "/path"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
Files to Modify
|
|
239
|
+
1. src/assets/js/core/query-strings.js (Major Refactor)
|
|
240
|
+
Current: 84 lines, captures UTM + affiliate only
|
|
241
|
+
|
|
242
|
+
Changes:
|
|
243
|
+
|
|
244
|
+
Add constants:
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
const ATTRIBUTION_KEY = 'attribution';
|
|
248
|
+
const FRESHNESS_DAYS = 30;
|
|
249
|
+
const FRESHNESS_MS = FRESHNESS_DAYS * 24 * 60 * 60 * 1000;
|
|
250
|
+
Add shouldPreserveAttribution(existingData) helper:
|
|
251
|
+
|
|
252
|
+
Returns true if existing data is < 30 days old (preserve first-touch)
|
|
253
|
+
Returns false if no data or data is >= 30 days old (allow overwrite)
|
|
254
|
+
Modify processAffiliateParams():
|
|
255
|
+
|
|
256
|
+
Add first-touch protection with shouldPreserveAttribution() check
|
|
257
|
+
Modify processUTMParams():
|
|
258
|
+
|
|
259
|
+
Add first-touch protection with shouldPreserveAttribution() check
|
|
260
|
+
Add processITMParams():
|
|
261
|
+
|
|
262
|
+
Mirror UTM structure but for itm_source, itm_medium, itm_campaign, itm_content
|
|
263
|
+
Include first-touch protection
|
|
264
|
+
Add processAdClickParams():
|
|
265
|
+
|
|
266
|
+
Capture fbclid, gclid, ttclid from URL params
|
|
267
|
+
Capture fbc from _fbc cookie
|
|
268
|
+
Include first-touch protection
|
|
269
|
+
Add getFbcCookie() helper:
|
|
270
|
+
|
|
271
|
+
Parse document.cookie for _fbc value
|
|
272
|
+
Add public API on webManager._ujLibrary.attribution:
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
{
|
|
276
|
+
get: () => getAttribution(), // Raw data (all, regardless of age)
|
|
277
|
+
getFresh: () => getFreshAttribution(), // Only < 30 days old
|
|
278
|
+
clear: () => clearAttribution() // Clear all attribution
|
|
279
|
+
}
|
|
280
|
+
Add getFreshAttribution():
|
|
281
|
+
|
|
282
|
+
Returns only attribution categories that are < 30 days old
|
|
283
|
+
This is the centralized freshness filter used by auth.js and session.js
|
|
284
|
+
2. src/assets/js/core/auth.js (Minor Update)
|
|
285
|
+
Current lines 176-187:
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
const attribution = webManager.storage().get('attribution', {});
|
|
289
|
+
const payload = {
|
|
290
|
+
affiliateCode: attribution.affiliate?.code || '',
|
|
291
|
+
attribution: attribution,
|
|
292
|
+
context: webManager.utilities().getContext(),
|
|
293
|
+
};
|
|
294
|
+
Change to:
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
const attribution = webManager.uj().attribution.getFresh();
|
|
298
|
+
const payload = {
|
|
299
|
+
affiliateCode: attribution.affiliate?.code || '',
|
|
300
|
+
attribution: attribution,
|
|
301
|
+
context: webManager.utilities().getContext(),
|
|
302
|
+
};
|
|
303
|
+
Uses centralized getFresh() to only send relevant (< 30 days) attribution
|
|
304
|
+
No other changes needed
|
|
305
|
+
3. src/assets/js/pages/payment/checkout/modules/session.js (Moderate Update)
|
|
306
|
+
Current lines 46-56 (ad-hoc 30-day check):
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
const utmData = webManager.storage().get('attribution.utm');
|
|
310
|
+
let utm = {};
|
|
311
|
+
if (utmData && utmData.timestamp && utmData.tags) {
|
|
312
|
+
const daysDiff = (new Date() - new Date(utmData.timestamp)) / (1000 * 60 * 60 * 24);
|
|
313
|
+
if (daysDiff < 30) {
|
|
314
|
+
utm = utmData.tags;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
Replace with:
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
const freshAttribution = webManager.uj().attribution.getFresh();
|
|
321
|
+
const utm = freshAttribution.utm?.tags || {};
|
|
322
|
+
Current lines 92-99 (UTM only in payment intent):
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
utm: {
|
|
326
|
+
source: utm.utm_source || '',
|
|
327
|
+
...
|
|
328
|
+
}
|
|
329
|
+
Expand to full attribution object:
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
// Full attribution tracking
|
|
333
|
+
attribution: {
|
|
334
|
+
utm: freshAttribution.utm ? {
|
|
335
|
+
source: freshAttribution.utm.tags.utm_source || '',
|
|
336
|
+
medium: freshAttribution.utm.tags.utm_medium || '',
|
|
337
|
+
campaign: freshAttribution.utm.tags.utm_campaign || '',
|
|
338
|
+
term: freshAttribution.utm.tags.utm_term || '',
|
|
339
|
+
content: freshAttribution.utm.tags.utm_content || ''
|
|
340
|
+
} : null,
|
|
341
|
+
itm: freshAttribution.itm ? {
|
|
342
|
+
source: freshAttribution.itm.tags.itm_source || '',
|
|
343
|
+
medium: freshAttribution.itm.tags.itm_medium || '',
|
|
344
|
+
campaign: freshAttribution.itm.tags.itm_campaign || '',
|
|
345
|
+
content: freshAttribution.itm.tags.itm_content || ''
|
|
346
|
+
} : null,
|
|
347
|
+
affiliate: freshAttribution.affiliate?.code || '',
|
|
348
|
+
adClicks: freshAttribution.adClicks ? {
|
|
349
|
+
fbclid: freshAttribution.adClicks.fbclid || '',
|
|
350
|
+
fbc: freshAttribution.adClicks.fbc || '',
|
|
351
|
+
gclid: freshAttribution.adClicks.gclid || '',
|
|
352
|
+
ttclid: freshAttribution.adClicks.ttclid || ''
|
|
353
|
+
} : null
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
// DEPRECATED: Keep for backwards compatibility
|
|
357
|
+
utm: {
|
|
358
|
+
source: utm.utm_source || '',
|
|
359
|
+
medium: utm.utm_medium || '',
|
|
360
|
+
campaign: utm.utm_campaign || '',
|
|
361
|
+
term: utm.utm_term || '',
|
|
362
|
+
content: utm.utm_content || ''
|
|
363
|
+
},
|
|
364
|
+
Implementation Order
|
|
365
|
+
query-strings.js - Core refactor with all new functionality
|
|
366
|
+
auth.js - Simple swap to use new API
|
|
367
|
+
session.js - Remove ad-hoc logic, expand attribution object
|
|
368
|
+
Public API Reference
|
|
369
|
+
After implementation, consumers use:
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
// Get all stored attribution (for debugging)
|
|
373
|
+
webManager.uj().attribution.get()
|
|
374
|
+
|
|
375
|
+
// Get only fresh attribution (< 30 days) - USE THIS
|
|
376
|
+
webManager.uj().attribution.getFresh()
|
|
377
|
+
|
|
378
|
+
// Clear all attribution data
|
|
379
|
+
webManager.uj().attribution.clear()
|
|
380
|
+
Testing Checklist
|
|
381
|
+
UTM params captured on landing
|
|
382
|
+
ITM params captured (test via exit popup link)
|
|
383
|
+
Affiliate code captured
|
|
384
|
+
Ad click IDs captured (fbclid, gclid, ttclid)
|
|
385
|
+
_fbc cookie read correctly
|
|
386
|
+
First-touch protected (second visit with new params doesn't overwrite)
|
|
387
|
+
30-day refresh works (simulated old timestamp allows overwrite)
|
|
388
|
+
getFresh() excludes stale data
|
|
389
|
+
Signup sends fresh attribution
|
|
390
|
+
Checkout includes full attribution object
|
|
391
|
+
Backend Considerations (Informational)
|
|
392
|
+
The backend should:
|
|
393
|
+
|
|
394
|
+
Store attribution on user document (rolling update on each sync)
|
|
395
|
+
Snapshot attribution on subscription document at purchase time
|
|
396
|
+
Use subscription attribution for recurring billing conversion events (server-side GA, Meta CAPI, TikTok Events API)
|
|
397
|
+
This is informational only - backend changes are out of scope for this implementation.
|
|
398
|
+
|
|
399
|
+
Stayed in plan mode
|
|
400
|
+
what about if we sync it when theres an update to localstorage instead of just signup and chekcout? this would go in query-string.js the conditions would be
|
|
401
|
+
|
|
402
|
+
1. there is a querystring attribution like utm, itm, ads, etc
|
|
403
|
+
2. the local attribution for whatever is in the query string is old and ready to be replaced
|
|
404
|
+
3. the user is signed in
|
|
405
|
+
|
|
406
|
+
then we would sync it with the server and overwrite whatever is new (so we could keep utm tags untouched and just update a fresh itm tag)
|
|
407
|
+
|
|
408
|
+
is that good?
|
|
409
|
+
|
|
410
|
+
we would update the local storage regardless of the user being signed in but we would sync onyl if they are signed in. we could even add a flag that denotes if it has been sync'd so that when the user signs in we can sync? i believe there is a similar thing for notifications: /Users/ian/Developer/Repositories/ITW-Creative-Works/web-manager/src/modules/notifications.js
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
first touch vs last touch idea
|
|
415
|
+
just to make sure we hve it down.
|
|
416
|
+
1. when someone lands with attribution querys tirngs and theres no attribution saved we can save the new ones no problem
|
|
417
|
+
2. if there are some saved, we overwrite only if the ones int he storage are 30 days old
|
|
418
|
+
3. we do this on a PER TYPE BASIS. so we can overwrite just the utm tags if its just those. if we came from a fb ad, and theres ither none stored or its too old, we can overwrite any queries from fb.
|
|
419
|
+
|
|
420
|
+
so basiclaly each set of queries is independant, we overwrite each set as long as its empty or too old, then we can use the most recent one to attribute to our conversion serverside
|
|
421
|
+
|
|
422
|
+
so should we make a separate field for each platform? or maybe we can just lump them all into "ads"?
|
|
423
|
+
|
|
424
|
+
also DO NOT BACKWARDS COMPATIBLE CHANGES, we can move forward with the replacement login (remove backwards shit)
|
|
425
|
+
|
|
426
|
+
so then would getfrresh (and thus the data we send to singup/purchase) be JUST the active attribution? or all? part of me wants to store everything we have access to in the user doc and then serverside we can decide if its recent enough to attribute to a signup? and thus we could also decide wahat to attach to the subscirprion document based on what is available at the time? so like if fb ad referred the user and they made a prchase within 30 days then that fbclid is forever attributed to each recurring payment even after 30 days?? right?
|
|
427
|
+
These are great refinements! Let me address each point:
|
|
428
|
+
|
|
429
|
+
1. Sync on Attribution Update (Not Just Signup/Checkout)
|
|
430
|
+
Yes, this is a good idea. The logic would be:
|
|
431
|
+
|
|
432
|
+
Always update localStorage when new attribution arrives (respecting 30-day first-touch rule)
|
|
433
|
+
Sync to server IF user is signed in AND something actually changed
|
|
434
|
+
Track sync status with a flag so we can sync when user eventually signs in
|
|
435
|
+
This is exactly how notifications work - I'll check that pattern.
|
|
436
|
+
|
|
437
|
+
Read notifications.js
|
|
438
|
+
I see the pattern with syncSubscription(). Now let me address your questions:
|
|
439
|
+
|
|
440
|
+
2. First-Touch vs Last-Touch - Per-Type Basis
|
|
441
|
+
Yes, you've got it exactly right:
|
|
442
|
+
|
|
443
|
+
Each type (UTM, ITM, affiliate, adClicks) is independent
|
|
444
|
+
We check freshness per type - if UTM is 30+ days old but affiliate is fresh, we overwrite UTM but keep affiliate
|
|
445
|
+
This gives you the best of both worlds - you preserve recent attribution per category
|
|
446
|
+
3. Separate Fields Per Ad Platform vs Lumped "adClicks"
|
|
447
|
+
I'd recommend lumping into "adClicks" because:
|
|
448
|
+
|
|
449
|
+
They all arrive on the same landing (user doesn't come from FB AND Google in the same click)
|
|
450
|
+
They share the same timestamp/freshness
|
|
451
|
+
Simpler storage structure
|
|
452
|
+
Server-side, you can still pick whichever ID is present (fbclid || gclid || ttclid)
|
|
453
|
+
If you ever need to track "came from FB, then later came from Google" - that's really a "separate journey" and the 30-day refresh handles that naturally.
|
|
454
|
+
|
|
455
|
+
4. What Should getFresh() Return?
|
|
456
|
+
This is the key question. Let me clarify the use cases:
|
|
457
|
+
|
|
458
|
+
Scenario What to Send
|
|
459
|
+
Signup ALL fresh attribution (for user doc)
|
|
460
|
+
Purchase ALL fresh attribution (snapshot to subscription doc)
|
|
461
|
+
Recurring billing (server) Use subscription doc's snapshotted attribution
|
|
462
|
+
So getFresh() should return all fresh data - that's what gets stored on user doc and snapshotted to subscription. Then server-side logic decides what's "recent enough" for conversion events.
|
|
463
|
+
|
|
464
|
+
Your understanding is correct: The fbclid snapshotted on subscription at purchase time is used for ALL recurring payments, even years later. That's the "attributed acquisition" for that subscription.
|
|
465
|
+
|
|
466
|
+
Let me update the plan with these refinements:
|
|
467
|
+
|
|
468
|
+
Edit woolly-stargazing-dove.md
|
|
469
|
+
Added 4 lines
|
|
470
|
+
Edit woolly-stargazing-dove.md
|
|
471
|
+
Added 6 lines
|
|
472
|
+
Now let me add the sync logic section:
|
|
473
|
+
|
|
474
|
+
Edit woolly-stargazing-dove.md
|
|
475
|
+
Added 17 lines
|
|
476
|
+
Edit woolly-stargazing-dove.md
|
|
477
|
+
Removed 9 lines
|
|
478
|
+
Edit woolly-stargazing-dove.md
|
|
479
|
+
Removed 37 lines
|
|
480
|
+
Edit woolly-stargazing-dove.md
|
|
481
|
+
Added 62 lines
|
|
482
|
+
The plan is now updated. Let me summarize the key refinements:
|
|
483
|
+
|
|
484
|
+
Summary of Updates
|
|
485
|
+
1. Sync on Attribution Change (not just signup/checkout)
|
|
486
|
+
|
|
487
|
+
When attribution changes AND user is signed in → sync immediately
|
|
488
|
+
When attribution changes AND user is NOT signed in → set _meta.needsSync = true
|
|
489
|
+
On sign-in, check needsSync flag and sync if true
|
|
490
|
+
2. Per-Type First-Touch Logic
|
|
491
|
+
|
|
492
|
+
Each category (UTM, ITM, affiliate, adClicks) is independent
|
|
493
|
+
Can overwrite stale UTM while keeping fresh affiliate
|
|
494
|
+
Each processor returns true/false to track if anything changed
|
|
495
|
+
3. Ad Click IDs Lumped into adClicks
|
|
496
|
+
|
|
497
|
+
fbclid, fbc, gclid, ttclid all share one timestamp
|
|
498
|
+
User can only arrive from one ad at a time
|
|
499
|
+
Simpler than separate per-platform tracking
|
|
500
|
+
4. getFresh() Returns ALL Fresh Data
|
|
501
|
+
|
|
502
|
+
Server receives complete fresh attribution
|
|
503
|
+
Server decides what to store on user doc vs subscription doc
|
|
504
|
+
Subscription gets a frozen snapshot at purchase time → used for all recurring billing conversions
|
|
505
|
+
5. Simplified auth.js and session.js
|
|
506
|
+
|
|
507
|
+
auth.js: Remove sendUserSignupMetadata() (query-strings handles sync now)
|
|
508
|
+
session.js: Just pass freshAttribution directly, no reshaping
|
|
509
|
+
Does this look good? Any other questions before I mark this ready for implementation?
|
|
510
|
+
|
|
511
|
+
Claude’s Plan
|
|
512
|
+
Attribution Tracking System - Implementation Plan
|
|
513
|
+
Overview
|
|
514
|
+
Build a comprehensive attribution tracking system that captures UTM tags, ITM tags (internal tracking), affiliate codes, and ad click IDs with a "first-touch with 30-day refresh" model.
|
|
515
|
+
|
|
516
|
+
Design Decisions
|
|
517
|
+
Attribution Model: First-touch with 30-day refresh, per-type basis
|
|
518
|
+
Each category (UTM, ITM, affiliate, adClicks) has independent freshness
|
|
519
|
+
Can overwrite stale UTM while preserving fresh affiliate, etc.
|
|
520
|
+
Freshness TTL: 30 days globally for all types
|
|
521
|
+
Server Sync: On attribution change (if signed in) + Signup + Checkout
|
|
522
|
+
Sync includes a needsSync flag for deferred sync when user signs in
|
|
523
|
+
Storage: User doc (rolling latest) + Subscription doc (snapshot at purchase)
|
|
524
|
+
No backwards compatibility - clean implementation
|
|
525
|
+
New Storage Structure
|
|
526
|
+
|
|
527
|
+
// localStorage key: "attribution"
|
|
528
|
+
{
|
|
529
|
+
utm: {
|
|
530
|
+
tags: { utm_source, utm_medium, utm_campaign, utm_term, utm_content },
|
|
531
|
+
timestamp: "ISO string",
|
|
532
|
+
url: "full landing URL",
|
|
533
|
+
page: "/path"
|
|
534
|
+
},
|
|
535
|
+
itm: {
|
|
536
|
+
tags: { itm_source, itm_medium, itm_campaign, itm_content },
|
|
537
|
+
timestamp: "ISO string",
|
|
538
|
+
url: "full URL",
|
|
539
|
+
page: "/path"
|
|
540
|
+
},
|
|
541
|
+
affiliate: {
|
|
542
|
+
code: "partner123",
|
|
543
|
+
timestamp: "ISO string",
|
|
544
|
+
url: "full URL",
|
|
545
|
+
page: "/path"
|
|
546
|
+
},
|
|
547
|
+
adClicks: {
|
|
548
|
+
// All ad click IDs lumped together (user can only arrive from one ad at a time)
|
|
549
|
+
fbclid: "from URL param",
|
|
550
|
+
fbc: "from _fbc cookie",
|
|
551
|
+
gclid: "from URL param",
|
|
552
|
+
ttclid: "from URL param",
|
|
553
|
+
timestamp: "ISO string",
|
|
554
|
+
url: "full URL",
|
|
555
|
+
page: "/path"
|
|
556
|
+
},
|
|
557
|
+
// Sync tracking
|
|
558
|
+
_meta: {
|
|
559
|
+
needsSync: false, // True if attribution changed while signed out
|
|
560
|
+
lastSynced: "ISO string" // Last successful sync to server
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
Files to Modify
|
|
564
|
+
1. src/assets/js/core/query-strings.js (Major Refactor)
|
|
565
|
+
Current: 84 lines, captures UTM + affiliate only
|
|
566
|
+
|
|
567
|
+
Changes:
|
|
568
|
+
|
|
569
|
+
Add constants:
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
const ATTRIBUTION_KEY = 'attribution';
|
|
573
|
+
const FRESHNESS_DAYS = 30;
|
|
574
|
+
const FRESHNESS_MS = FRESHNESS_DAYS * 24 * 60 * 60 * 1000;
|
|
575
|
+
Add shouldPreserveAttribution(existingData) helper:
|
|
576
|
+
|
|
577
|
+
Returns true if existing data is < 30 days old (preserve first-touch)
|
|
578
|
+
Returns false if no data or data is >= 30 days old (allow overwrite)
|
|
579
|
+
Modify processAffiliateParams():
|
|
580
|
+
|
|
581
|
+
Add first-touch protection with shouldPreserveAttribution() check
|
|
582
|
+
Return true if data was updated, false otherwise
|
|
583
|
+
Modify processUTMParams():
|
|
584
|
+
|
|
585
|
+
Add first-touch protection with shouldPreserveAttribution() check
|
|
586
|
+
Return true if data was updated, false otherwise
|
|
587
|
+
Add processITMParams():
|
|
588
|
+
|
|
589
|
+
Mirror UTM structure but for itm_source, itm_medium, itm_campaign, itm_content
|
|
590
|
+
Include first-touch protection
|
|
591
|
+
Return true if data was updated, false otherwise
|
|
592
|
+
Add processAdClickParams():
|
|
593
|
+
|
|
594
|
+
Capture fbclid, gclid, ttclid from URL params
|
|
595
|
+
Capture fbc from _fbc cookie
|
|
596
|
+
Include first-touch protection
|
|
597
|
+
Return true if data was updated, false otherwise
|
|
598
|
+
Add getFbcCookie() helper:
|
|
599
|
+
|
|
600
|
+
Parse document.cookie for _fbc value
|
|
601
|
+
Add public API on webManager._ujLibrary.attribution:
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
{
|
|
605
|
+
get: () => getAttribution(), // Raw data (all, regardless of age)
|
|
606
|
+
getFresh: () => getFreshAttribution(), // Only < 30 days old
|
|
607
|
+
sync: () => syncAttribution(), // Manual sync to server
|
|
608
|
+
clear: () => clearAttribution() // Clear all attribution
|
|
609
|
+
}
|
|
610
|
+
Add getFreshAttribution():
|
|
611
|
+
|
|
612
|
+
Returns only attribution categories that are < 30 days old
|
|
613
|
+
Excludes _meta from output
|
|
614
|
+
This is what gets sent to server (signup, checkout, sync)
|
|
615
|
+
Add syncAttribution() function:
|
|
616
|
+
|
|
617
|
+
Called when any attribution category is updated
|
|
618
|
+
Checks if user is signed in:
|
|
619
|
+
If signed in: POST to server immediately, update _meta.lastSynced
|
|
620
|
+
If not signed in: Set _meta.needsSync = true
|
|
621
|
+
Uses authorizedFetch to send to backend-manager with command user:attribution-sync
|
|
622
|
+
Add auth state listener for deferred sync:
|
|
623
|
+
|
|
624
|
+
On auth state change (user signs in), check _meta.needsSync
|
|
625
|
+
If true, call syncAttribution() to catch up
|
|
626
|
+
2. src/assets/js/core/auth.js (Simplify)
|
|
627
|
+
The signup metadata logic can be simplified since attribution sync now happens automatically in query-strings.js.
|
|
628
|
+
|
|
629
|
+
Changes:
|
|
630
|
+
|
|
631
|
+
Remove sendUserSignupMetadata() function entirely
|
|
632
|
+
|
|
633
|
+
Attribution sync is now handled by query-strings.js on auth state change
|
|
634
|
+
The user:sign-up command becomes user:attribution-sync (same endpoint, different command)
|
|
635
|
+
Keep setAnalyticsUserId() as-is (still needed for GA/FB/TikTok user identification)
|
|
636
|
+
|
|
637
|
+
The auth listener no longer needs to call sendUserSignupMetadata()
|
|
638
|
+
|
|
639
|
+
Note: If there's other signup-specific logic needed (beyond attribution), we can keep a slimmed-down version. But if it's just attribution, the query-strings.js sync handles it.
|
|
640
|
+
|
|
641
|
+
3. src/assets/js/pages/payment/checkout/modules/session.js (Simplify)
|
|
642
|
+
Current lines 46-56 (ad-hoc 30-day check):
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
const utmData = webManager.storage().get('attribution.utm');
|
|
646
|
+
let utm = {};
|
|
647
|
+
if (utmData && utmData.timestamp && utmData.tags) {
|
|
648
|
+
const daysDiff = (new Date() - new Date(utmData.timestamp)) / (1000 * 60 * 60 * 24);
|
|
649
|
+
if (daysDiff < 30) {
|
|
650
|
+
utm = utmData.tags;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
Replace with:
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
const freshAttribution = webManager.uj().attribution.getFresh();
|
|
657
|
+
Current lines 92-99 - Replace entire utm block with:
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
// Attribution tracking (all fresh data for server to snapshot)
|
|
661
|
+
attribution: freshAttribution,
|
|
662
|
+
That's it. The server receives the full fresh attribution object and decides how to store it on the subscription document. No need to reshape/flatten here - keep it clean.
|
|
663
|
+
|
|
664
|
+
Implementation Order
|
|
665
|
+
query-strings.js - Core refactor with all new functionality + sync logic
|
|
666
|
+
auth.js - Remove signup metadata (handled by query-strings sync now)
|
|
667
|
+
session.js - Simplify to just pass freshAttribution to server
|
|
668
|
+
Sync Flow Diagram
|
|
669
|
+
|
|
670
|
+
User lands with ?utm_source=google
|
|
671
|
+
│
|
|
672
|
+
▼
|
|
673
|
+
┌─────────────────────────────┐
|
|
674
|
+
│ processAttribution() │
|
|
675
|
+
│ - Check each type freshness │
|
|
676
|
+
│ - Update localStorage │
|
|
677
|
+
│ - Track what changed │
|
|
678
|
+
└─────────────────────────────┘
|
|
679
|
+
│
|
|
680
|
+
▼ (if anything changed)
|
|
681
|
+
┌─────────────────────────────┐
|
|
682
|
+
│ Is user signed in? │
|
|
683
|
+
├──────────┬──────────────────┤
|
|
684
|
+
│ YES │ NO │
|
|
685
|
+
│ │ │ │ │
|
|
686
|
+
│ ▼ │ ▼ │
|
|
687
|
+
│ syncNow()│ needsSync=true │
|
|
688
|
+
└──────────┴──────────────────┘
|
|
689
|
+
│
|
|
690
|
+
▼ (later, user signs in)
|
|
691
|
+
┌─────────────────────────────┐
|
|
692
|
+
│ Auth state listener fires │
|
|
693
|
+
│ - Check needsSync flag │
|
|
694
|
+
│ - If true, syncNow() │
|
|
695
|
+
│ - Clear flag │
|
|
696
|
+
└─────────────────────────────┘
|
|
697
|
+
Public API Reference
|
|
698
|
+
|
|
699
|
+
// Get all stored attribution (for debugging)
|
|
700
|
+
webManager.uj().attribution.get()
|
|
701
|
+
|
|
702
|
+
// Get only fresh attribution (< 30 days) - USE THIS
|
|
703
|
+
webManager.uj().attribution.getFresh()
|
|
704
|
+
|
|
705
|
+
// Manually trigger sync to server (usually automatic)
|
|
706
|
+
webManager.uj().attribution.sync()
|
|
707
|
+
|
|
708
|
+
// Clear all attribution data
|
|
709
|
+
webManager.uj().attribution.clear()
|
|
710
|
+
Server-Side Data Flow (Informational)
|
|
711
|
+
|
|
712
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
713
|
+
│ USER DOCUMENT │
|
|
714
|
+
│ /users/{uid} │
|
|
715
|
+
│ ┌────────────────────────────────────────────────────────┐ │
|
|
716
|
+
│ │ attribution: { │ │
|
|
717
|
+
│ │ utm: { tags: {...}, timestamp }, │ │
|
|
718
|
+
│ │ itm: { tags: {...}, timestamp }, │ │
|
|
719
|
+
│ │ affiliate: { code, timestamp }, │ │
|
|
720
|
+
│ │ adClicks: { fbclid, fbc, gclid, ttclid, timestamp } │ │
|
|
721
|
+
│ │ } │ │
|
|
722
|
+
│ └────────────────────────────────────────────────────────┘ │
|
|
723
|
+
│ ↑ Rolling update on each sync │
|
|
724
|
+
└──────────────────────────────────────────────────────────────┘
|
|
725
|
+
|
|
726
|
+
│
|
|
727
|
+
│ On purchase, snapshot to:
|
|
728
|
+
▼
|
|
729
|
+
|
|
730
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
731
|
+
│ SUBSCRIPTION DOCUMENT │
|
|
732
|
+
│ /users/{uid}/subscriptions/{subId} │
|
|
733
|
+
│ ┌────────────────────────────────────────────────────────┐ │
|
|
734
|
+
│ │ attribution: { ... } ← FROZEN at purchase time │ │
|
|
735
|
+
│ └────────────────────────────────────────────────────────┘ │
|
|
736
|
+
│ Used for ALL recurring billing conversion events │
|
|
737
|
+
│ (even years later - this is the "acquisition attribution") │
|
|
738
|
+
└──────────────────────────────────────────────────────────────┘
|
|
739
|
+
Why this matters: When a recurring payment happens 6 months later, the server uses the subscription's frozen attribution.adClicks.fbclid to send a Meta CAPI event, attributing revenue to the original ad campaign.
|
|
740
|
+
|
|
741
|
+
Testing Checklist
|
|
742
|
+
UTM params captured on landing
|
|
743
|
+
ITM params captured (test via exit popup link)
|
|
744
|
+
Affiliate code captured
|
|
745
|
+
Ad click IDs captured (fbclid, gclid, ttclid)
|
|
746
|
+
_fbc cookie read correctly
|
|
747
|
+
First-touch protected per-type (second visit doesn't overwrite fresh data)
|
|
748
|
+
30-day refresh works per-type (stale category gets overwritten)
|
|
749
|
+
getFresh() excludes stale data
|
|
750
|
+
Sync fires when signed in + attribution changes
|
|
751
|
+
needsSync flag set when signed out + attribution changes
|
|
752
|
+
Deferred sync fires on sign-in when needsSync is true
|
|
753
|
+
Checkout includes full freshAttribution object
|