Skip to content

Commit 38b74ab

Browse files
authored
Merge pull request #396 from topcoder-platform/dev
[PROD] - UTM & updates
2 parents 23b4bfc + d52eabe commit 38b74ab

File tree

6 files changed

+283
-1
lines changed

6 files changed

+283
-1
lines changed

src/lib/components/user-area/UserArea.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { AUTH0_AUTHENTICATOR_URL } from 'lib/config';
66
import { AUTH_USER_ROLE } from 'lib/config/auth';
77
import { DISABLE_NUDGES } from "lib/config/profile-toasts.config";
8+
import { appendUtmParamsToUrl } from 'lib/functions/utm-cookies.handler';
89
910
import ToolSelector from '../tool-selector/ToolSelector.svelte';
1011
@@ -65,12 +66,15 @@
6566
function onSignIn(signup?: any) {
6667
const locationHref = `${window.location.origin}${window.location.pathname}`
6768
68-
const signupUrl = [
69+
let signupUrl = [
6970
`${AUTH0_AUTHENTICATOR_URL}?retUrl=${encodeURIComponent(locationHref)}`,
7071
signup === true ? '&mode=signUp' : '',
7172
$ctx.signupUtmCodes,
7273
].filter(Boolean).join('&')
7374
75+
// Append UTM parameters from cookie if they exist
76+
signupUrl = appendUtmParamsToUrl(signupUrl);
77+
7478
window.location.href = signupUrl;
7579
}
7680

src/lib/config/nav-menu/all-nav-items.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,4 +217,9 @@ export const allNavItems: {[key: string]: NavMenuItem} = {
217217
marketingPathname: '/talent',
218218
url: getMarketingUrl('/talent'),
219219
},
220+
referralProgram: {
221+
label: 'Referral Programme',
222+
marketingPathname: '/nasa-referral',
223+
url: getMarketingUrl('/nasa-referral'),
224+
},
220225
}

src/lib/config/nav-menu/main-navigation.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const mainNavigationItems: NavMenuItem[] = [
2222
allNavItems.aiHub,
2323
allNavItems.statistics,
2424
allNavItems.discordApp,
25+
allNavItems.referralProgram,
2526
]
2627
},
2728
{
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import { TC_DOMAIN } from '../config/hosts';
2+
import { getEnvValue } from '../config/env-vars';
3+
4+
// UTM cookie configuration types
5+
interface UtmParams {
6+
utm_source?: string;
7+
utm_medium?: string;
8+
utm_campaign?: string;
9+
}
10+
11+
// Cookie configuration constants
12+
const TC_UTM_COOKIE_NAME = 'tc_utm';
13+
const DEFAULT_COOKIE_LIFETIME_DAYS = 3;
14+
const COOKIE_PATH = '/';
15+
const COOKIE_SAMESITE = 'Lax';
16+
17+
/**
18+
* Sanitizes a string to remove all characters except A-Z, a-z, 0-9, hyphen (-), underscore (_)
19+
* @param input - The string to sanitize
20+
* @returns Sanitized string
21+
*/
22+
export function sanitize(input: string): string {
23+
if (!input || typeof input !== 'string') {
24+
return '';
25+
}
26+
// Remove all characters except A-Z, a-z, 0-9, hyphen (-), underscore (_)
27+
return input.replace(/[^A-Za-z0-9\-_]/g, '');
28+
}
29+
30+
/**
31+
* Extracts and sanitizes UTM parameters from the URL
32+
* @returns Object containing sanitized utm_source, utm_medium, utm_campaign
33+
*/
34+
function extractUtmParams(): UtmParams {
35+
const params: UtmParams = {};
36+
37+
try {
38+
const searchParams = new URLSearchParams(window.location.search);
39+
40+
const utm_source = searchParams.get('utm_source');
41+
const utm_medium = searchParams.get('utm_medium');
42+
const utm_campaign = searchParams.get('utm_campaign');
43+
44+
if (utm_source) {
45+
params.utm_source = sanitize(utm_source);
46+
}
47+
if (utm_medium) {
48+
params.utm_medium = sanitize(utm_medium);
49+
}
50+
if (utm_campaign) {
51+
params.utm_campaign = sanitize(utm_campaign);
52+
}
53+
} catch (error) {
54+
console.warn('Error extracting UTM parameters:', error);
55+
}
56+
57+
return params;
58+
}
59+
60+
/**
61+
* Gets the cookie lifetime from environment variable or uses default
62+
* @returns Lifetime in days
63+
*/
64+
function getCookieLifetimeDays(): number {
65+
try {
66+
const envValue = getEnvValue<string>('VITE_UTM_COOKIE_LIFETIME_DAYS', String(DEFAULT_COOKIE_LIFETIME_DAYS));
67+
const days = parseInt(envValue, 10);
68+
return isNaN(days) ? DEFAULT_COOKIE_LIFETIME_DAYS : days;
69+
} catch {
70+
return DEFAULT_COOKIE_LIFETIME_DAYS;
71+
}
72+
}
73+
74+
/**
75+
* Gets the cookie domain with leading dot for broader subdomain coverage
76+
* @returns Cookie domain (e.g., .topcoder.com)
77+
*/
78+
function getCookieDomain(): string {
79+
return `.${TC_DOMAIN}`;
80+
}
81+
82+
/**
83+
* Checks if a cookie with the given name exists
84+
* @param name - Cookie name
85+
* @returns true if cookie exists, false otherwise
86+
*/
87+
function cookieExists(name: string): boolean {
88+
const cookies = document.cookie.split(';');
89+
return cookies.some(cookie => cookie.trim().startsWith(`${name}=`));
90+
}
91+
92+
/**
93+
* Sets a cookie with the specified attributes
94+
* @param name - Cookie name
95+
* @param value - Cookie value
96+
* @param options - Cookie options (domain, path, sameSite, secure, maxAge)
97+
*/
98+
function setCookie(
99+
name: string,
100+
value: string,
101+
options: {
102+
domain?: string;
103+
path?: string;
104+
sameSite?: string;
105+
secure?: boolean;
106+
maxAge?: number;
107+
} = {}
108+
): void {
109+
const {
110+
domain = getCookieDomain(),
111+
path = COOKIE_PATH,
112+
sameSite = COOKIE_SAMESITE,
113+
secure = true,
114+
maxAge = DEFAULT_COOKIE_LIFETIME_DAYS * 24 * 60 * 60, // Convert days to seconds
115+
} = options;
116+
117+
let cookieString = `${name}=${encodeURIComponent(value)}`;
118+
119+
if (domain) {
120+
cookieString += `; domain=${domain}`;
121+
}
122+
if (path) {
123+
cookieString += `; path=${path}`;
124+
}
125+
if (maxAge) {
126+
cookieString += `; max-age=${maxAge}`;
127+
}
128+
if (sameSite) {
129+
cookieString += `; SameSite=${sameSite}`;
130+
}
131+
if (secure) {
132+
cookieString += '; Secure';
133+
}
134+
135+
document.cookie = cookieString;
136+
}
137+
138+
/**
139+
* Initializes UTM cookie handling on page load
140+
* Extracts UTM parameters from URL, sanitizes them, and persists to cookie
141+
* Only sets the cookie if it doesn't already exist
142+
*/
143+
export function initializeUtmCookieHandler(): void {
144+
try {
145+
// Check if cookie already exists
146+
if (cookieExists(TC_UTM_COOKIE_NAME)) {
147+
console.debug('UTM cookie already exists, skipping initialization');
148+
return;
149+
}
150+
151+
// Extract and sanitize UTM parameters
152+
const utmParams = extractUtmParams();
153+
154+
// Only set cookie if we have at least one UTM parameter
155+
if (Object.keys(utmParams).length === 0) {
156+
console.debug('No UTM parameters found in URL');
157+
return;
158+
}
159+
160+
// Create JSON value with all UTM parameters
161+
const cookieValue = JSON.stringify(utmParams);
162+
163+
// Get cookie lifetime in seconds
164+
const lifetimeDays = getCookieLifetimeDays();
165+
const maxAgeSecs = lifetimeDays * 24 * 60 * 60;
166+
167+
// Set the cookie with proper attributes
168+
setCookie(TC_UTM_COOKIE_NAME, cookieValue, {
169+
domain: getCookieDomain(),
170+
path: COOKIE_PATH,
171+
sameSite: COOKIE_SAMESITE,
172+
secure: true,
173+
maxAge: maxAgeSecs,
174+
});
175+
176+
console.debug(`UTM cookie set successfully:`, utmParams);
177+
} catch (error) {
178+
console.error('Error initializing UTM cookie handler:', error);
179+
}
180+
}
181+
182+
/**
183+
* Retrieves and parses the tc_utm cookie
184+
* @returns Parsed UTM parameters or null if cookie doesn't exist
185+
*/
186+
export function getUtmCookie(): UtmParams | null {
187+
try {
188+
const cookies = document.cookie.split(';');
189+
const cookieStr = cookies.find(cookie => cookie.trim().startsWith(`${TC_UTM_COOKIE_NAME}=`));
190+
191+
if (!cookieStr) {
192+
return null;
193+
}
194+
195+
const cookieValue = decodeURIComponent(cookieStr.split('=')[1]);
196+
return JSON.parse(cookieValue) as UtmParams;
197+
} catch (error) {
198+
console.warn('Error retrieving UTM cookie:', error);
199+
return null;
200+
}
201+
}
202+
203+
/**
204+
* Appends UTM parameters from the tc_utm cookie to a given URL
205+
* Only appends parameters that exist in the cookie
206+
* @param url - The base URL to append parameters to
207+
* @returns URL with UTM parameters appended, or original URL if no cookie exists
208+
*/
209+
export function appendUtmParamsToUrl(url: string): string {
210+
if (!url) {
211+
return url;
212+
}
213+
214+
const utmParams = getUtmCookie();
215+
if (!utmParams || Object.keys(utmParams).length === 0) {
216+
return url;
217+
}
218+
219+
try {
220+
const urlObj = new URL(url, window.location.origin);
221+
222+
// Append only the UTM parameters that exist in the cookie
223+
if (utmParams.utm_source) {
224+
urlObj.searchParams.set('utm_source', utmParams.utm_source);
225+
}
226+
if (utmParams.utm_medium) {
227+
urlObj.searchParams.set('utm_medium', utmParams.utm_medium);
228+
}
229+
if (utmParams.utm_campaign) {
230+
urlObj.searchParams.set('utm_campaign', utmParams.utm_campaign);
231+
}
232+
233+
return urlObj.toString();
234+
} catch (error) {
235+
console.warn('Error appending UTM parameters to URL:', error);
236+
return url;
237+
}
238+
}

src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Writable } from 'svelte/store'
33

44
import { buildContext, type AuthUser, type NavigationHandler, type SupportMeta } from './lib/app-context'
55
import { PubSub } from './lib/utils/pubsub';
6+
import { initializeUtmCookieHandler } from './lib/functions/utm-cookies.handler';
67

78
import 'lib/styles/main.scss';
89

@@ -207,4 +208,7 @@ function execQueueCall(method: TcUniNavMethods, ...args: unknown[]) {
207208
// replace the method that adds the calls to the queue
208209
// with a direct exec call
209210
Object.assign(window as any, {[globalName]: execQueueCall.bind(null)});
211+
212+
// Initialize UTM cookie handler on module load
213+
initializeUtmCookieHandler();
210214
})()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
interface UtmParams {
2+
utm_source?: string;
3+
utm_medium?: string;
4+
utm_campaign?: string;
5+
}
6+
/**
7+
* Sanitizes a string to remove all characters except A-Z, a-z, 0-9, hyphen (-), underscore (_)
8+
* @param input - The string to sanitize
9+
* @returns Sanitized string
10+
*/
11+
export declare function sanitize(input: string): string;
12+
/**
13+
* Initializes UTM cookie handling on page load
14+
* Extracts UTM parameters from URL, sanitizes them, and persists to cookie
15+
* Only sets the cookie if it doesn't already exist
16+
*/
17+
export declare function initializeUtmCookieHandler(): void;
18+
/**
19+
* Retrieves and parses the tc_utm cookie
20+
* @returns Parsed UTM parameters or null if cookie doesn't exist
21+
*/
22+
export declare function getUtmCookie(): UtmParams | null;
23+
/**
24+
* Appends UTM parameters from the tc_utm cookie to a given URL
25+
* Only appends parameters that exist in the cookie
26+
* @param url - The base URL to append parameters to
27+
* @returns URL with UTM parameters appended, or original URL if no cookie exists
28+
*/
29+
export declare function appendUtmParamsToUrl(url: string): string;
30+
export {};

0 commit comments

Comments
 (0)