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.
Files changed (29) hide show
  1. package/.claude/settings.local.json +3 -2
  2. package/README.md +0 -2
  3. package/TODO-ATTRIBUTION2.md +753 -0
  4. package/bin/ultimate-jekyll +1 -1
  5. package/dist/assets/js/core/auth.js +37 -32
  6. package/dist/assets/js/core/exit-popup.js +3 -0
  7. package/dist/assets/js/core/query-strings.js +16 -12
  8. package/dist/assets/js/libs/auth.js +12 -0
  9. package/dist/assets/js/libs/form-manager.js +1 -1
  10. package/dist/assets/js/pages/account/sections/api-keys.js +1 -4
  11. package/dist/assets/js/pages/account/sections/connections.js +11 -17
  12. package/dist/assets/js/pages/account/sections/delete.js +5 -15
  13. package/dist/assets/js/pages/account/sections/security.js +2 -6
  14. package/dist/assets/js/pages/admin/notifications/new/index.js +5 -16
  15. package/dist/assets/js/pages/download/index.js +3 -6
  16. package/dist/assets/js/pages/oauth2/index.js +5 -5
  17. package/dist/assets/js/pages/payment/checkout/modules/session.js +1 -1
  18. package/dist/assets/js/pages/token/index.js +1 -5
  19. package/dist/assets/themes/bootstrap/overrides/_avatars.scss +5 -1
  20. package/dist/assets/themes/bootstrap/overrides/_buttons-adaptive.scss +68 -55
  21. package/dist/assets/themes/bootstrap/overrides/_color-shades.scss +25 -0
  22. package/dist/assets/themes/bootstrap/overrides/_index.scss +1 -0
  23. package/dist/assets/themes/classy/css/components/_buttons.scss +1 -2
  24. package/dist/build.js +1 -1
  25. package/dist/defaults/dist/pages/test/libraries/bootstrap.html +7 -0
  26. package/dist/defaults/src/_config.yml +10 -6
  27. package/dist/gulp/tasks/defaults.js +1 -1
  28. package/dist/gulp/tasks/sass.js +4 -0
  29. 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