Skip to content
6 changes: 5 additions & 1 deletion src/lib/components/user-area/UserArea.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { AUTH0_AUTHENTICATOR_URL } from 'lib/config';
import { AUTH_USER_ROLE } from 'lib/config/auth';
import { DISABLE_NUDGES } from "lib/config/profile-toasts.config";
import { appendUtmParamsToUrl } from 'lib/functions/utm-cookies.handler';

import ToolSelector from '../tool-selector/ToolSelector.svelte';

Expand Down Expand Up @@ -65,12 +66,15 @@
function onSignIn(signup?: any) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The signup parameter is typed as any, which can lead to potential type-related issues. Consider using a more specific type, such as boolean | undefined, to improve type safety and maintainability.

const locationHref = `${window.location.origin}${window.location.pathname}`

const signupUrl = [
let signupUrl = [
`${AUTH0_AUTHENTICATOR_URL}?retUrl=${encodeURIComponent(locationHref)}`,
signup === true ? '&mode=signUp' : '',
$ctx.signupUtmCodes,
].filter(Boolean).join('&')

// Append UTM parameters from cookie if they exist
signupUrl = appendUtmParamsToUrl(signupUrl);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Ensure that the appendUtmParamsToUrl function correctly handles URLs that already contain query parameters, to avoid malformed URLs. This is crucial for maintaining the correctness of the URL redirection.


window.location.href = signupUrl;
}

Expand Down
5 changes: 5 additions & 0 deletions src/lib/config/nav-menu/all-nav-items.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,9 @@ export const allNavItems: {[key: string]: NavMenuItem} = {
marketingPathname: '/talent',
url: getMarketingUrl('/talent'),
},
referralProgram: {
label: 'Referral Programme',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
Consider using consistent spelling for 'Programme' as 'Program' to match the rest of the codebase and ensure uniformity across the application.

marketingPathname: '/nasa-referral',
url: getMarketingUrl('/nasa-referral'),
},
}
1 change: 1 addition & 0 deletions src/lib/config/nav-menu/main-navigation.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const mainNavigationItems: NavMenuItem[] = [
allNavItems.aiHub,
allNavItems.statistics,
allNavItems.discordApp,
allNavItems.referralProgram,
]
},
{
Expand Down
238 changes: 238 additions & 0 deletions src/lib/functions/utm-cookies.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { TC_DOMAIN } from '../config/hosts';
import { getEnvValue } from '../config/env-vars';

// UTM cookie configuration types
interface UtmParams {
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
}

// Cookie configuration constants
const TC_UTM_COOKIE_NAME = 'tc_utm';
const DEFAULT_COOKIE_LIFETIME_DAYS = 3;
const COOKIE_PATH = '/';
const COOKIE_SAMESITE = 'Lax';

/**
* Sanitizes a string to remove all characters except A-Z, a-z, 0-9, hyphen (-), underscore (_)
* @param input - The string to sanitize
* @returns Sanitized string
*/
export function sanitize(input: string): string {
if (!input || typeof input !== 'string') {
return '';
}
// Remove all characters except A-Z, a-z, 0-9, hyphen (-), underscore (_)
return input.replace(/[^A-Za-z0-9\-_]/g, '');
}

/**
* Extracts and sanitizes UTM parameters from the URL
* @returns Object containing sanitized utm_source, utm_medium, utm_campaign
*/
function extractUtmParams(): UtmParams {
const params: UtmParams = {};

try {
const searchParams = new URLSearchParams(window.location.search);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
Using window.location directly ties this function to a browser environment, which may limit testing or reuse in non-browser contexts. Consider passing the URL as a parameter to increase flexibility.


const utm_source = searchParams.get('utm_source');
const utm_medium = searchParams.get('utm_medium');
const utm_campaign = searchParams.get('utm_campaign');

if (utm_source) {
params.utm_source = sanitize(utm_source);
}
if (utm_medium) {
params.utm_medium = sanitize(utm_medium);
}
if (utm_campaign) {
params.utm_campaign = sanitize(utm_campaign);
}
} catch (error) {
console.warn('Error extracting UTM parameters:', error);
}

return params;
}

/**
* Gets the cookie lifetime from environment variable or uses default
* @returns Lifetime in days
*/
function getCookieLifetimeDays(): number {
try {
const envValue = getEnvValue<string>('VITE_UTM_COOKIE_LIFETIME_DAYS', String(DEFAULT_COOKIE_LIFETIME_DAYS));
const days = parseInt(envValue, 10);
return isNaN(days) ? DEFAULT_COOKIE_LIFETIME_DAYS : days;
} catch {
return DEFAULT_COOKIE_LIFETIME_DAYS;
}
}

/**
* Gets the cookie domain with leading dot for broader subdomain coverage
* @returns Cookie domain (e.g., .topcoder.com)
*/
function getCookieDomain(): string {
return `.${TC_DOMAIN}`;
}

/**
* Checks if a cookie with the given name exists
* @param name - Cookie name
* @returns true if cookie exists, false otherwise
*/
function cookieExists(name: string): boolean {
const cookies = document.cookie.split(';');
return cookies.some(cookie => cookie.trim().startsWith(`${name}=`));
}

/**
* Sets a cookie with the specified attributes
* @param name - Cookie name
* @param value - Cookie value
* @param options - Cookie options (domain, path, sameSite, secure, maxAge)
*/
function setCookie(
name: string,
value: string,
options: {
domain?: string;
path?: string;
sameSite?: string;
secure?: boolean;
maxAge?: number;
} = {}
): void {
const {
domain = getCookieDomain(),
path = COOKIE_PATH,
sameSite = COOKIE_SAMESITE,
secure = true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ security]
The secure option is hardcoded to true, which means this code will not work over HTTP. Consider making this configurable or documenting that this function requires HTTPS.

maxAge = DEFAULT_COOKIE_LIFETIME_DAYS * 24 * 60 * 60, // Convert days to seconds
} = options;

let cookieString = `${name}=${encodeURIComponent(value)}`;

if (domain) {
cookieString += `; domain=${domain}`;
}
if (path) {
cookieString += `; path=${path}`;
}
if (maxAge) {
cookieString += `; max-age=${maxAge}`;
}
if (sameSite) {
cookieString += `; SameSite=${sameSite}`;
}
if (secure) {
cookieString += '; Secure';
}

document.cookie = cookieString;
}

/**
* Initializes UTM cookie handling on page load
* Extracts UTM parameters from URL, sanitizes them, and persists to cookie
* Only sets the cookie if it doesn't already exist
*/
export function initializeUtmCookieHandler(): void {
try {
// Check if cookie already exists
if (cookieExists(TC_UTM_COOKIE_NAME)) {
console.debug('UTM cookie already exists, skipping initialization');
return;
}

// Extract and sanitize UTM parameters
const utmParams = extractUtmParams();

// Only set cookie if we have at least one UTM parameter
if (Object.keys(utmParams).length === 0) {
console.debug('No UTM parameters found in URL');
return;
}

// Create JSON value with all UTM parameters
const cookieValue = JSON.stringify(utmParams);

// Get cookie lifetime in seconds
const lifetimeDays = getCookieLifetimeDays();
const maxAgeSecs = lifetimeDays * 24 * 60 * 60;

// Set the cookie with proper attributes
setCookie(TC_UTM_COOKIE_NAME, cookieValue, {
domain: getCookieDomain(),
path: COOKIE_PATH,
sameSite: COOKIE_SAMESITE,
secure: true,
maxAge: maxAgeSecs,
});

console.debug(`UTM cookie set successfully:`, utmParams);
} catch (error) {
console.error('Error initializing UTM cookie handler:', error);
}
}

/**
* Retrieves and parses the tc_utm cookie
* @returns Parsed UTM parameters or null if cookie doesn't exist
*/
export function getUtmCookie(): UtmParams | null {
try {
const cookies = document.cookie.split(';');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 correctness]
Splitting document.cookie by ; assumes that cookies do not contain semicolons, which is generally safe but not guaranteed. Consider using a more robust cookie parsing library if this becomes an issue.

const cookieStr = cookies.find(cookie => cookie.trim().startsWith(`${TC_UTM_COOKIE_NAME}=`));

if (!cookieStr) {
return null;
}

const cookieValue = decodeURIComponent(cookieStr.split('=')[1]);
return JSON.parse(cookieValue) as UtmParams;
} catch (error) {
console.warn('Error retrieving UTM cookie:', error);
return null;
}
}

/**
* Appends UTM parameters from the tc_utm cookie to a given URL
* Only appends parameters that exist in the cookie
* @param url - The base URL to append parameters to
* @returns URL with UTM parameters appended, or original URL if no cookie exists
*/
export function appendUtmParamsToUrl(url: string): string {
if (!url) {
return url;
}

const utmParams = getUtmCookie();
if (!utmParams || Object.keys(utmParams).length === 0) {
return url;
}

try {
const urlObj = new URL(url, window.location.origin);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
Using window.location.origin assumes the code is running in a browser environment. Consider passing the base URL as a parameter to make this function more versatile.


// Append only the UTM parameters that exist in the cookie
if (utmParams.utm_source) {
urlObj.searchParams.set('utm_source', utmParams.utm_source);
}
if (utmParams.utm_medium) {
urlObj.searchParams.set('utm_medium', utmParams.utm_medium);
}
if (utmParams.utm_campaign) {
urlObj.searchParams.set('utm_campaign', utmParams.utm_campaign);
}

return urlObj.toString();
} catch (error) {
console.warn('Error appending UTM parameters to URL:', error);
return url;
}
}
4 changes: 4 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Writable } from 'svelte/store'

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

import 'lib/styles/main.scss';

Expand Down Expand Up @@ -207,4 +208,7 @@ function execQueueCall(method: TcUniNavMethods, ...args: unknown[]) {
// replace the method that adds the calls to the queue
// with a direct exec call
Object.assign(window as any, {[globalName]: execQueueCall.bind(null)});

// Initialize UTM cookie handler on module load
initializeUtmCookieHandler();
})()
30 changes: 30 additions & 0 deletions types/src/lib/functions/utm-cookies.handler.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
interface UtmParams {
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
}
/**
* Sanitizes a string to remove all characters except A-Z, a-z, 0-9, hyphen (-), underscore (_)
* @param input - The string to sanitize
* @returns Sanitized string
*/
export declare function sanitize(input: string): string;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider specifying the behavior of the sanitize function when the input contains characters that are not A-Z, a-z, 0-9, hyphen (-), or underscore (_). This will improve the clarity of the function's contract.

/**
* Initializes UTM cookie handling on page load
* Extracts UTM parameters from URL, sanitizes them, and persists to cookie
* Only sets the cookie if it doesn't already exist
*/
export declare function initializeUtmCookieHandler(): void;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The initializeUtmCookieHandler function should specify the expected behavior if the URL does not contain any UTM parameters. This will help ensure consistent handling of edge cases.

/**
* Retrieves and parses the tc_utm cookie
* @returns Parsed UTM parameters or null if cookie doesn't exist
*/
export declare function getUtmCookie(): UtmParams | null;
/**
* Appends UTM parameters from the tc_utm cookie to a given URL
* Only appends parameters that exist in the cookie
* @param url - The base URL to append parameters to
* @returns URL with UTM parameters appended, or original URL if no cookie exists
*/
export declare function appendUtmParamsToUrl(url: string): string;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Clarify the behavior of appendUtmParamsToUrl when the url parameter already contains UTM parameters. This will help avoid potential conflicts or duplication of parameters.

export {};
Loading