$v) { if (is_array($v) ) { if (isset($allowedArrayFields[$k]) && $allowedArrayFields[$k] == 1) { $event[$k] = $v; } continue; } $key = mb_strtolower($k, "UTF-8"); if (strlen($key) <= $maxKeyLength && strlen($v) <= $maxValueLength) { $event[$key] = $v; } } } return (array)$event; } /** * this function will validate the get parameters are allowed (as these can be sent from the client side they need to be verified) * also their value must have a reasonable length * @return array */ private static function buildValidGet() : array { $maxKeyLength = 40; $maxValueLength = 250; $privacy = new Privacy(); #event array $event = []; # register all get parameters and remove crappy stuff $allowedGetFields = self::allowedGetFields(); if ($_GET) { foreach ($_GET as $k => $v) { $key = mb_strtolower($k, "UTF-8"); # prevent stored XSS via the simplest possible method... just remove any tags that # might be present! $v = strip_tags($v); # accents $normalizedV = iconv('UTF-8','ASCII//TRANSLIT',$v); # We need to make sure to validate the input we are providing to UET # We cannot store user data inside of UET without first encoding the data # This section checks whether the value provided is an email and hashes it if (filter_var($normalizedV, FILTER_VALIDATE_EMAIL)) { $response = $privacy->secureEmail($normalizedV); if($response['email_valid'] === 'yes' && isset($response['hash'])){ $v = $response['hash']; } } # prevent any crazy key to be inserted if (strlen($key) <= $maxKeyLength && strlen($v) <= $maxValueLength && $allowedGetFields[$key]) { $typeFormat = $allowedGetFields[$key]; # if variable type is string > confirm GET is string AND not null if($typeFormat == 'string' ){ if(is_string($v) && $v != ''){ $event["get_$key"] = $v; } # if GET field type is int > confirm it is int AND not null } elseif($typeFormat == 'int'){ if(is_numeric($v) && $v != ''){ $event["get_$key"] = (int)$v; } } } } # decoding the warning from the original job before the alternate job redirect #(isset($x["get_from_warning"])) ? $x["get_from_warning"] = flo($x["get_from_warning"]) : ""; } return (array)$event; } /** * this function will clean the cookies as well as these can also be modified from the client side * also their value must have a reasonable length * @return array */ private static function buildValidCacheInfo() : array { # NOTE: If you are not seeing your entry in UET, confirm that it does not exceed the max char. count!! $maxKeyLength = 40; $maxValueLength = 200; if (isset($_GET["templateId"])) { $_SESSION["uet_template_id"] = $_GET["templateId"]; } if (isset($_GET["virtualSignatureName"])) { $_SESSION["uet_sam_virtual_name"] = $_GET["virtualSignatureName"]; } if (isset($_GET["samsSendWay"])) { $_SESSION["uet_sam_send_way"] = $_GET["samsSendWay"]; } if (isset($_GET["testid"])) { $_SESSION["uet_testid"] = (string) $_GET["testid"]; } if (isset($_GET["group_name"])) { $_SESSION["uet_group_name"] = (string) $_GET["group_name"]; } if (isset($_GET["test_version"])) { $_SESSION["uet_test_version"] = (string) $_GET["test_version"]; } if (isset($_GET["search_id"])) { $_SESSION["uet_search_id"] = (string) $_GET["search_id"]; } if (isset($_GET["sq"])) { $_SESSION["uet_sq"] = (string) $_GET["sq"]; } #event array $event = []; # register all uet cookies and remove crappy stuff $allowedCacheFields = self::allowedCacheFields(); # track cookies starting with uet_ if ($_SESSION) { foreach ($_SESSION as $k => $v) { $key = mb_strtolower($k, "UTF-8"); if (is_string($v)) { # prevent stored XSS via the simplest possible method... just remove any tags that # might be present! $v = strip_tags($v); } if (substr($k, 0, 4) == "uet_" && strlen($k) <= $maxKeyLength && strlen($v ?? "") <= $maxValueLength && $allowedCacheFields[$key] == 1) { $label = str_replace("uet_", 'cache_', $k); $event[$label] = $v; } } } # add the GA session replication to UET if ( isset($_SESSION['ga_replica']['key']) && !empty($_SESSION['ga_replica']['key']) ) { $event['ga_replica'] = $_SESSION['ga_replica']['key']; } #todo verify with stas for flow approval if (isset($_SESSION["redirect_cpa_info"]["cpa_flow_id"]) && !empty($_SESSION["redirect_cpa_info"]["cpa_flow_id"]) ) { $event['flow_id'] = $_SESSION["redirect_cpa_info"]["cpa_flow_id"]; } return (array)$event; } /** * this function will clean the cookies as well as these can also be modified from the client side * also their value must have a reasonable length * @return array */ private static function buildValidCookies() : array { $maxKeyLength = 40; $maxValueLength = 60; #event array $event = []; # register all uet cookies and remove crappy stuff $allowedCookieFields = self::allowedCookieFields(); # track cookies starting with uet_ if ($_COOKIE) { foreach ($_COOKIE as $k => $v) { $key = mb_strtolower($k, "UTF-8"); if ( substr($k, 0, 4) == "uet_" && strlen($k) <= $maxKeyLength && strlen($v) <= $maxValueLength && isset($allowedCookieFields[$key]) && $allowedCookieFields[$key] == 1 ) { $label = str_replace("uet_", 'cookie_', $k); $event[$label] = $v; } } } return (array)$event; } /** * wrapper for building the event * @param $eventType * @param $additionalParams * @return array */ private static function buildEvent(string $eventType, array $additionalParams) : array { $get = self::buildValidGet(); $cookies = self::buildValidCookies(); $cacheInfo = self::buildValidCacheInfo(); # try setting the session cache $sessionInfo = []; $additionalParams = self::validateParameters($additionalParams); $meta = self::buildMeta($eventType, $additionalParams); $metaTag = (Array) $_SESSION['ABCore']; $metaTagDemo = (Array) $_SESSION['ABCoreDemo']; $metaTagABTest = (Array) $_SESSION['ABTest']; $event = array_merge($meta, $get, $cookies, $cacheInfo, $sessionInfo, $metaTag, $metaTagDemo, $metaTagABTest, $additionalParams); return $event; } /** * From the UUID we will generate a percentage for any user segregation * @param $uuid * @return mixed */ public static function randomNumberGenerator($uuid){ //We will only use the first two letters of the UUID $hex = substr($uuid, 0, 2); //Translate that two letters from hexadecimal to int $seed = hexdec($hex); // The maximum possible number that $seed can become is 255 (#FF) so that's the denominator of our percentage $percentage = ($seed / 255) * 100; $percentage = round($percentage,2); //Return all the needed data and debug data $out["hex"] = $hex; $out["seed"] = $seed; $out["percentage"] = $percentage; return $out; } /** * This will extract the root folder from the given URI, example /myRoot/otherPage?variable=abc * it will return 'myRoot' as a string * @param String $URI * @return mixed|string */ private static function extractRootFolderURI($URI,$currentLanguage = ""){ //Parsing the string to be and URL array $base = parse_url($URI); //Validate if the variable provided was a valid URI if(!$base["path"]){ return "invalid URI"; } //the base becomes the path and we explode by / $base = $base["path"]; $base = explode('/',$base); //We take the first variable that is not empty and is not the current laguange foreach ($base as $key => $value){ if($value != "" && $value != $currentLanguage){ $base = $value; break; } } // and we remove any Query from this and return it $base = explode('?',(string)$base); if($base[0] == "Array"){ $processRoot = ""; }else{ $processRoot = $base[0]; } return $processRoot; } /** * Extract the language from the URL either from the folder url structure or the $_REQUEST language if * the call is been call inside the AJAX folder * @return mixed|string */ public static function extractLanguageFromURL(){ //we need the country TLD to extract the country from the URL $domains = Init::countryArguments(); //Language extraction process we need the REDIRECT_URL Ex: /fr/view, /jobs, etc. $base = $_SERVER["REDIRECT_URL"]; //Explode the base will help us know the possible language selected $languageFinder = explode("/",$base); $possibleLanguage = ""; $currentLanguage = ""; // We loop the $languageFinder array from the explode and check if the string length is 2 then we might have a // candidate foreach ($languageFinder as $k =>$v){ if(strlen((string)$v) == 2){ $possibleLanguage = $v; } } // For the next step of extracting the language we need the current domain Ex: ca, www, de, etc $currentDomain = $_SERVER["HTTP_HOST"]; //We remove any Dev trace from the URL Ex: william-dev-ca.talent.com will be transform to ca.talent.com $currentDomain = explode("-",$currentDomain); $currentDomain = end($currentDomain); //Now we only have to remove the rest by an explode to '.' we will finally have the "ca" or "fr" $currentDomain = explode('.',$currentDomain); $currentDomain = $currentDomain[0]; //Fallback if the domain is not something that makes sense we set www as the domain if(!$domains[$currentDomain]){ $currentDomain = "www"; } //Now with the $currentDomain we get the domain language settings and verify if our possibleLanguage is a valid // one $languageSettings = $domains[$currentDomain]["languages"]; foreach ( $languageSettings as $k => $v){ //If we find a match then we take update the $currentLanguage variable if($v == $possibleLanguage){ $currentLanguage = $v; } } //The only exception were we explicit send the language as a $_REQUEST parameter is over the /ajax/ calls if(isset($_REQUEST["language"]) && strpos($base, "/ajax/") !== false){ $currentLanguage = $_REQUEST["language"]; } //We assume that if there is no math for the $currentLanguage then is the default_language of the domain if($currentLanguage == ""){ $currentLanguage = $domains[$currentDomain]["default_language"]; } return $currentLanguage; } /** * this creates the basic/default meta that should be integrated in every event * @param $eventType * @param $additionalParams * @return mixed */ private static function buildMeta(string $eventType, array $additionalParams) : array { #event array $x = []; # define the event type $x["_event"] = $eventType; #validation to set the real application date just for auto_apply #since this event is being set from the auto_apply cron //this cant be done... in time base databases we cant define "time" of our choices, date is current timestamp PERIOD. if we need the real application date, create a new field, then learn how to aggregate by IT // if(!$x["application_date"]){ // $date = time(); // }else{ // $date = $x["application_date"]; // unset($x["application_date"]); // } # define a unique user tracking parameter $x["uuid"] = self::getUUID(); $x["nuuid"] = self::genNUUID(); if (isset($additionalParams["nuuid_falcon"]) && !empty($additionalParams["nuuid_falcon"])) { $x["nuuid"] = $additionalParams["nuuid_falcon"]; } $x["company_context"] = self::$context; $x["uri"] = $_SERVER["REQUEST_URI"]; $x["referer"] = $_SERVER["HTTP_REFERER"] ?? 'no_referer'; $x["uri"] = self::validateURLCompliance($_SERVER["REQUEST_URI"]); $x["referer"] = self::validateURLCompliance($_SERVER["HTTP_REFERER"]); if (!empty($_COOKIE["ttc"])) { $x["ttc"] = $_COOKIE["ttc"]; } # patch for ensuring that we do not leave a null field when referer is missing if(!$x["referer"]){ $x["referer"] = "no_referer"; } $x["host"] = $_SERVER["HTTP_HOST"]; # in various cases, ie coreg & google for jobs tracking pixel, the SERVER info ignores the 'www' literal # in this case, our numbers suffer because www.talent.com doesn't receive the data it needs # this is a litle hacky, but could be a silver bullet solution moving forward if($x["host"] == "talent.com"){$x["host"] = "www.talent.com";} $x["scheme"] = $_SERVER["REQUEST_SCHEME"]; $x["server_ip"] = $_SERVER["SERVER_ADDR"]; #masking the ip to comply with GDPR $tmpIp = (new Privacy())->secureIp($_SERVER["REMOTE_ADDR"]); $x["ip"] = $tmpIp["encrypted"]; $x["ip_hash"] = $tmpIp["hash"]; $x["ip_mask"] = $tmpIp["mask"]; $x["useragent"] = $_SERVER["HTTP_USER_AGENT"]; $x["device"] = self::getMobileDevice(); $x['is_mobile'] = $x['device'] == 'desktop' ? 'no' : 'yes'; $x["date"] = Tools::zuluDate(); if (isset($additionalParams["date_falcon"]) && !empty($additionalParams["date_falcon"])) { $x["date"] = $additionalParams["date_falcon"]; } #$x["whoami"] = !$whoami ? gethostname() : $whoami; #overwrite get_country | get_language if the custom parameter had been provided if (isset($additionalParams["country"])) $x["get_country"] = $additionalParams["country"]; if (isset($additionalParams["language"])) $x["get_language"] = $additionalParams["language"]; /* CLOUDFLARE START */ # store the bot likeliness score coming from cloudflare if(isset($_SERVER['HTTP_TLN_X_BOT_SCORE'])){ $x['cf_bot_score'] = (int) $_SERVER['HTTP_TLN_X_BOT_SCORE']; } /* CLOUDFLARE STOP */ return $x; } /** * We need to ensure that we aren't storing any sensitive data inside of UET * And that includes the URLs that are dynamically stored in UET * In this method I am parsing the URL, checking whether something is an email, and if it is, * I am hashing the email and throwing it back in the URL * @param $url */ private static function validateURLCompliance($url){ $privacy = new Privacy(); if($url ==''){ return $url; } # split out the URL into the path and uri components # set each into their according variable $urlSplit = preg_split("/\?/", $url); $path = $urlSplit[0] ?? ""; $uri = $urlSplit[1] ?? ""; # if there are no URI params, return the path, we're done if(!$uri){ return $path; } # split the uri part of the string into the GET key and value pairs parse_str( $uri, $urlGETParams); # loop over every pair and check whether the value is an email # if it is, use our secureEmail method to hash the email value foreach($urlGETParams as $key=>&$value){ $normalizedV = iconv('UTF-8','ASCII//TRANSLIT',$value); if (filter_var($normalizedV, FILTER_VALIDATE_EMAIL)) { $response = $privacy->secureEmail($normalizedV); if($response['email_valid'] === 'yes' && isset($response['hash'])){ $value = $response['hash']; } } } # build out the URI and then combine it back with the path $uri = http_build_query($urlGETParams); $url = "{$path}?{$uri}"; return $url; } /** * this is the user hook, this function should wrap and trigger any user related event * to update the table on the user side * @param $event * @param $additionalParams * @throws Exception */ private static function jobseekerHook($event){ # these two method are replaced by the method below. Once we know everything is running smoothly I will # remove this from the code! // self::addEventToUser($event); // self::sendUserValue($event); # to prevent updating jobSeeker twice for every user-action, we have combined the methods that: # 1) append the user value to the lifetime value counter # 2) append the event into one of the arrays within jobSeeker # into one method. self::updateJobSeeker($event); } /** * This method builds the parameters that I need in order to update the jobSeeker table * None of the updates happen within this method, it is simply a feeder for the smart function within JobSeeker * this method will then point to a method within jobSeeker that will replace the following methods by combining them into one:: -incrementPropertyStats -addUserEvent * @param $event * @return false */ private static function updateJobSeeker($event){ # check whether the user is signed in with a cookie, or browsing remotely on another device from an SMS/email if(!empty($GLOBALS['jobseeker']['id'])){ $userId = $GLOBALS['jobseeker']['id']; }elseif(!empty($GLOBALS['remote_jobseeker']['id'])){ $userId = $GLOBALS['remote_jobseeker']['id']; }else{ # a user ID of some sorts is required to make this work! return false; } # Step 2 :: Build the array that will update the job_click array within jobseeker # Currently job_click is the only array that gets updated through this method, the rest are in manageUserEvent.php $clickData = []; $type = "not_set"; # through UET we will only append the job_click array, none of the rest pass through this method! if($event['_event'] == 'jobredirect') { $type = "job_click"; # use the UET job_click event to build an array to send over to jobSeeker to update the jobSeeker Arrays + soc info! $dataResult = self::buildJobSeekerDataArray($event); if ($dataResult['result'] == 'ok') { $clickData = $dataResult['payload']; } } # Step 3 :: Calculate the Value this event is bringing us. # It will either be a increment of a count, or an increment for a value if the event is a click! $eventValueCalculation = self::determineValue($event); $eventValueArray = $eventValueCalculation['payload']; # There is no reason to go into the jobSeeker method and try to update the user... when there is nothing to update! # this little guy here trims 300-900ms per page for the SERP depending on the user's proximity to our servers... if($eventValueArray["property"] == '' && $type = 'not_set'){ return false; } # Step 4 :: Send to jobSeeker for it to handle the rest! $js = new JobSeeker2(); $addEvent = $js->addUserEvent((string)$userId,(string)$type,(array)$clickData, (array)$eventValueArray, true); return true; } /** * Using the event data coming into UET, try to determine what kind of event this is, and how it fits into the * user lifetime-value matrix. The lifetime value can either be an incrementation of the count of visits to a particular * page, or is can also increment the value of the ppc of the job when coming from a jobRedirect. * @param $event * @return array|false|string */ private static function determineValue($event){ $value = 0; $property = null; $propertyField = null; $talentEmailSources = ['email', 'talent-email', 'sam-talent-email']; switch ($event['_event']) { case 'pageview': switch ($event['pagetype']) { case 'taxCal': $property = 'tax'; $propertyField = 'count'; break; case 'salarySearch': $property = 'salary'; $propertyField = 'count'; break; case 'converter': $property = 'converter'; $propertyField = 'count'; break; } break; case 'jobredirect': if(in_array($event['get_source'], $talentEmailSources)){ $property = 'email'; $propertyField = 'value'; }elseif($event['get_source'] == 'mobileapp'){ $property = 'mobileapp'; $propertyField = 'value'; }elseif($event['get_source'] == 'whatsapp'){ $property = 'whatsapp'; $propertyField = 'value'; }elseif($event['get_source'] == 'sms'){ $property = 'sms'; $propertyField = 'value'; }elseif($event['get_context'] == 'serp-whitepage'){ $property = 'related'; $propertyField = 'value'; }elseif($event['get_context'] == 'serp'){ $property = 'serp'; $propertyField = 'value'; }else{ # this other will not create a new column in the value matrix # it will simply go towards incrementing the total lifetime count/value! $property = 'other'; $propertyField = 'value'; } $value = $event['net_billed_ppc_cad']/10; break; } $result['property'] = $property; $result['propertyField'] = $propertyField; $result['value'] = $value; return Api::return_result($result); } /** * We add data into arrays within jobSeeker about the last 20 actions the user has taken * Inside of this UET bridge, this method prepares the fields that I will need to send over to jobSeeker * This data will also be used to determine the latest soc_interest information within jobSeeker as well * @param $event * @return array|false|string */ public static function buildJobSeekerDataArray($event){ $data["keyword"] = $event['job_title']; $data["location"] = $event['job_location']; $data["empname"] = $event['job_empname']; $data["jobid"] = $event['job_id']; $data["source"] = $event['cache_source']; $data["get_source"] = $event['get_source']; $data["templateid"] = $event['get_templateid']; $data["remote"] = $event["job_geo_remote"]; # these are used primarily to help update the "interest_soc" fields $data["soc_code"] = $event['job_soc_onetsoc_code']; $data["soc_minor_group"] = $event['job_soc_minor_group']; $data["soc_major_group"] = $event['job_soc_major_group']; $data["soc_detailed_group"] = $event['job_soc_detailed_group']; $data["soc_broad_group"] = $event['job_soc_broad_group']; $data["soc_matched_title"] = $event['job_soc_matched_title']; $data["soc_classifier"] = $event['job_soc_classifier']; $data["ppc"] = $event['job_ppc']; $data["ppc_u"] = $event['job_ppc_u']; $data["talent_ppc"] = $event['job_talent_ppc']; $data["talent_ppc_u"] = $event['job_talent_ppc_u']; # we need the charged data, we want to exclude the arbitrage clicks $data["charged_is_valid"] = $event['charged_is_valid']; # Adding country into data in order use it when we're creating the event id $data["country"] = strtolower($event['job_geo_country']); return Api::return_result($data); } /** * If you need to store something in the Session cache you need to actually set the value into the SESSION variable * This method provides a centralized place where you can do that that comes after all of the methods that correct the * various GET parameters * Keep in mind you also need to allow the parameters you created to dynamically go into UET via allowedSessionFields method */ public static function setSessionCache() { # track the context in a session cache to know what was the last talent.com page the user passed through! if($_GET["context"] != ""){ $_SESSION['uet_context'] = $_GET['context']; } # For A/B testing - If $_GET["user_group"] is not empty, then add this field to the SESSION. if (!empty($_GET["user_group"])) { $_SESSION["uet_user_group"] = $_GET["user_group"]; } ################ Store 3rd Party User Tokens For User Value Tracking ################### # Adwords User/Click Token if(!empty($_GET["gclid"])){ $_SESSION["uet_gclid"] = $_GET["gclid"]; } # Facebook Ads User/Click Token if(!empty($_GET["fbclid"])){ $_SESSION["uet_fbclid"] = $_GET["fbclid"]; } # Bing Ads User/Click Token if(!empty($_GET["msclkid"])){ $_SESSION["uet_msclkid"] = $_GET["msclkid"]; } # SEM Tracking if(!empty($_GET["obclid"])){ $_SESSION["uet_obclid"] = $_GET["obclid"]; } ################ Store UTM information from Google & Various Partners ################### if(!empty($_GET["utm_source"])){ $_SESSION["uet_utm_source"] = $_GET["utm_source"]; } if(!empty($_GET["utm_campaign"])){ $_SESSION["uet_utm_campaign"] = $_GET["utm_campaign"]; } if(!empty($_GET["utm_medium"])){ $_SESSION["uet_utm_medium"] = $_GET["utm_medium"]; } ################ Store diverted traffic from various landing pages to our main Website ########## if(!empty($_GET["dtf"])){ $_SESSION["uet_dtf"] = $_GET["dtf"]; } ########################## store subid for partners ################## if(!empty($_GET["subid"])){ $_SESSION["uet_subid"] = $_GET["subid"]; } if( !empty($_GET["source"]) && empty($_GET["subid"]) && ( !isset($_SERVER["HTTP_REFERER"]) || !preg_match("#talent\.com#siu", $_SERVER["HTTP_REFERER"]) ) ) { unset($_SESSION["uet_subid"]); } } private static function setMobileAppParams(&$additionalParams) { // If is_mobile GET param is set, then replace the value if (isset($_GET['is_mobile'])) { $additionalParams["is_mobile"] = (string)$_GET['is_mobile']; } // If is not set, then put 1 (web) as default if (isset($additionalParams['is_web'])) { if (in_array($additionalParams['api_device'], ApiAuth::USER_DEVICES_APP)) { $additionalParams['is_web'] = 0; } else { $additionalParams['is_web'] = 1; } } else { $additionalParams['is_web'] = 1; } } /** * this function will get the label for the product and set it on the cache_product field in uet * @param $type */ private static function determineProduct($type) { switch ($type) { case "whitepage": case "job_description": $product = 'job_description_page'; break; case "qualityLab": case str_contains($type, 'White') && $type != 'whitepage': case str_contains($type, 'white') && $type != 'whitepage' && !str_contains($type, 'mbg'): $product = 'job_description_page_monetize'; break; case "serp": $product = 'serp'; break; case str_contains($type, 'mbg'): case str_contains($type, 'dtl'): case 'location': case 'questions': $product = 'serp_monetize'; break; case "taxCal": $product = 'other_tax'; break; case "salarySearch": $product = 'other_salary'; break; case "home": $product = 'other_home'; break; case "converter": $product = 'other_converter'; break; default: $product = 'other'; break; } $_SESSION["uet_product"] = $product; } /** * set the cache_product field, this will get the label for the product and set it on the cache_product field in uet * it will require the redirect_type or the pagetype to be set * also this will reset the cache_product field when the get_source is present * @param $eventType * @param $additionalParams */ private static function setProductParams($eventType, $additionalParams) { if(!isset($_SESSION['uet_product']) && $eventType == 'jobredirect'){ if($additionalParams['redirect_type'] != 'source') { self::determineProduct($additionalParams['redirect_type']); } } if(!isset($_SESSION['uet_product']) && isset($additionalParams['pagetype'])) { self::determineProduct($additionalParams['pagetype']); } if ( isset($_SESSION['uet_product']) && isset($_GET['source']) && $additionalParams['redirect_type'] != 'source' ) { if(isset($additionalParams['pagetype'])) { self::determineProduct($additionalParams['pagetype']); } if($eventType == 'jobredirect') { self::determineProduct($additionalParams['redirect_type']); } } } /** * creates an event in uet * @param string $eventType * @param null $additionalParams * @return array|string * @throws Exception */ public static function registerEvent(string $eventType = 'pageview', array $additionalParams = null) { $partnerAccount = new PartnerAccount(); $es = Elastic2::instance('write'); #set the globals if they exist as extra parameters and overwrite them self::setGlobals($additionalParams); self::setMobileAppParams($additionalParams); self::setProductParams($eventType, $additionalParams); # Certain events, like job-redirect are self-sufficient, while others, like page-view need to be supplemented # with additional information, like the page-type, to know the actual event name $fullEventName = $eventType; if($additionalParams['pagetype'] != ''){ $fullEventName = "{$eventType}_{$additionalParams['pagetype']}"; } # make sure to track where the user is coming from within our own website self::trackContext($fullEventName); $callBackPaths = "(googleCallback.php|facebookCallback.php|linkedinCallback.php)"; if( !preg_match("/{$callBackPaths}/siu", $_SERVER["REQUEST_URI"]) && empty($_GET["uit"]) && !(isset($_GET['apply_source']) && $_GET['apply_source'] == "appcast" && isset($_GET['appid'])) ){ # make sure to track source if necessary self::trackSource(); } # convert various GET parameters into SESSION variables to dynamically get stored in UET self::setSessionCache(); #build event // Sending null values to this method will cause that fatal error, prevent it by using an "ARRAY" cast $event = self::buildEvent($eventType, (array)$additionalParams); # trigger the jobseeker hook event self::jobseekerHook($event); #increment transaction at the user if exits. #self::sendUserValue($event); # append columns to every event from the GLOBALS array $event = self::setFieldsFromGlobals($event); # these params were needed for the jobSeeker historic table, but should not be inserted into uet # unset them to prevent adding another column to UET for no reason! unset($event['job_geo_lat'], $event['job_geo_lon']); # do not register event for bots $userAgentTest = Compliance::isUserAgentCompliant($event["useragent"]); # If the click is a pageview, track google bot and other none compliant crawlers. if($eventType == 'pageview'){ # If googlebot... track pageviews if(preg_match("#googlebot#siu",$_SERVER["HTTP_USER_AGENT"])){ $event["_event"] = "pageview_bot"; $userAgentTest["compliant"] = "yes"; } # If agent not compliant ... track as crawlers... if($userAgentTest["compliant"]=="no"){ $event["_event"] = "pageview_crawler"; $userAgentTest["compliant"] = "yes"; } } # If its api, we need to check which of the fields has the publisher token, else we send the source. $source = (((isset($_GET["publisher"]) || (isset($_GET["utm_medium"]))) && $_GET["source"] == "api")) ? ($_GET["publisher"] ?? $_GET["utm_medium"]) : $_GET["source"] ?? $_GET["utm_source"]; # If the event is correct and the source is not blacklisted, start the postback publisher process. if ($eventType == "user-create" || $eventType == "quality_apply" || $eventType == "quality_view" /*&& ($partnerAccount->isBlacklisted($source))*/) { // We need to do this because coreg register is a post call (so no user agent) // Also, we cannot put it on isUserAgentCompliant because we need jobseeker_context if ($event["jobseeker_context"] == "coreg") { $userAgentTest["compliant"] = "yes"; } # Check if this event and pbc_id are eligible for postback publisher process. $isPostbackPublisher = $partnerAccount->isPostbackPublisher($eventType, $_GET["pbc_id"] ?? $_SESSION["pbc_id"])["payload"]; # If the user is new, the registration date in global will be empty, we modify it with the current date if ($isPostbackPublisher[0]) { if (!isset($event["registration_date"])) { $event["registration_date"] = Tools::zuluDate(); } # Validate which click id param name is coming through the url. $clickIdParam = ""; $clickIdParams = ["clickid","cid", "clid", "ob_click_id", "tid", "xslcid", "clickID", "click_id"]; $intersect = array_intersect($clickIdParams, array_keys($_GET)); if(!empty($intersect)){ $clickIdParam = array_shift($intersect); } # Postback the conversion to the partner $r = $partnerAccount->sendPostbackInformation($_SESSION[$clickIdParam] ?? $_GET[$clickIdParam], $_SESSION["pbc_id"] ?? $_GET["pbc_id"], $_GET["job_id"], "", $eventType, $isPostbackPublisher[1]); # When successfully sent the postback, we want to store a new event in UET. $params["event_trigger"] = $eventType; $params["ucid"] = $_GET["ucid"]; $params["click_id"] = $_SESSION[$clickIdParam] ?? $_GET[$clickIdParam]; # Save publisher postback event self::registerEvent("publisher_postback", $params); # Check if the job has feed_type for storing in the event if ((!empty($event["job_id"]) || !empty($_SESSION["job_id"])) && empty($event["job_feed_type"])) { $jobId = !empty($event["job_id"]) ? $event["job_id"] : $_SESSION["job_id"]; $country = Init::siteCountry(); if ($event["country"]) { $country = $event["country"]; } elseif ($event["job_geo_country"]) { $country = $event["job_geo_country"]; } $jobInfo = Jobs::getJob($jobId, $country, "feed_type"); if ($jobInfo["data"]["job"]) { $event["job_feed_type"] = $jobInfo["data"]["job"]["feed_type"]; } } } } # If the user comes from coreg and it's an emailcoreg event we need to force useragent as compliant # postback is another case where we need to ensure that the event is inserted despite a lack of useragent # since it's not passed through coreg if ( $eventType == 'emailcoreg' || ($eventType == "conversion" && $event["source"] == "talent_postback") || $eventType == "tmp_coreg_tracking" || strpos($_SERVER["HTTP_USER_AGENT"], "fb.me/webhooks") !== false ) { $userAgentTest["compliant"] = "yes"; } /* We call the method "createTalentApplyID" in order to create a session for storing a unique id for getting billing information * in jobSeekerApply file * For more information, this Session is being consumed in 3.1-ProcessRequest and jobSeekerApply files */ if($eventType == "jobredirect" && $event["job_apply_type"] == "talentApply") { self::createTalentApplyID($event["job_id"]); $event['apid'] = $_SESSION['talentApplyID']; } if ($userAgentTest["compliant"] == "yes") { # store the date in UTC as well $event["dateUTC"] = gmdate('Y-m-d\TH:i:s\Z'); # You should never ever over-write the date if (!isset($event["date_falcon"]) || empty($event["date_falcon"])) { $event["date"] = Tools::zuluDate(); } if (isset($event["date_falcon_utc"]) && !empty($event["date_falcon_utc"])) { $event["dateUTC"] = $additionalParams["date_falcon_utc"]; } # enrich the event with additional parameters coming from other data sources $event = self::enrich($event); $response = $es->insert("uet", $event, 0); $streamer = new EventStreamer(); $streamer->streamEvent($event); } return $response; } /** * I'm noticing more and more teams enriching the uet events before insertion and it is muddying up the registerEvent method * This method aims to serve as a central hub for all enriching needs * The format must be as follows: * -each enrichment must be a method that is called line by line in this method * -each enrichment method must pass-by-value the event array, append fields to it, and return the same event array * -each enrichment must be heavily cached, and CANNOT perform queries to data sources directly without a cache interface * @param array $event * @return array */ private static function enrich( array $event ) : array { try { $event = self::addPartnerAccountOwner($event); $event = self::addGoogleCrawlerIdentification($event); $event = self::addDealId($event); } catch(Throwable $e){} return $event; } /** Add the Deal id of the campaign to the event, this will retrieve the dea id from the campaigns table in sql * @param array $event * @return array */ private static function addDealId( array $event ) : array { if(!empty($event['job_campaign_id'])) { $dealId = self::getDealIdFromCampaignId($event['job_campaign_id']); if(!empty($dealId)) { $event['deal_id'] = $dealId; } } return $event; } /** function to get the deal id from the campaigns table * this will use mem cache, so we don't have to always query * the database * * the cache will be set for 24 hours * @param $campaignId * @return array|false|mixed|string */ private static function getDealIdFromCampaignId($campaignId) { $memcache = Cache::instance(); $cacheVersion = "V1"; $keyCached = 'deal_id_campaign_id' . $cacheVersion . $campaignId; $dealId = $memcache->get($keyCached); if (empty($dealId)) { $sql = Sql::instance('read'); $query = "SELECT hubspotDealID FROM talent_employers.campaigns WHERE campaignID = '{$sql->clean($campaignId)}'"; $result = $sql->dbQuery($query); $dealId = $result['data'][0]['hubspotDealID']; $memcache->set($keyCached, $dealId, 60 * 60 * 24); } return $dealId; } /** * To drive more traffic to our site we "partner" with various other institutions that send traffic our way * Each of these partners has an employee who serves as the "account owner" * @param array $event * @return array */ private static function addPartnerAccountOwner( array $event ) : array { # all of these events must be enriched with the partner account owner $eventsRequiringProductOwnerField = [ "quality_view", "quality_scroll", "quality_apply", "quality_form", "quality_os_ip_time", "dtl-click", "emailcoreg", "jobredirect", ]; if( in_array($event["_event"],$eventsRequiringProductOwnerField) ){ $result = (new PartnerAccount())->getAccountAndProductDataBySource(); if(isset($result["accountOwner"]) && !empty($result["accountOwner"])){ $event["partner_account_owner"] = (string) $result["accountOwner"]; } } return $event; } /** * It is very easy to spoof the useragent, and we are discovering that our identification of Google needs to be more robust * Thus, we will use a list of IP addresses coming from Google to determine whether the crawl request truly is from Google * @param array $event * @return array */ private static function addGoogleCrawlerIdentification( array $event ) : array { $memcache = Cache::instance(); $cacheVersion = "V4"; $keyCached = "googleIps" . $cacheVersion; $v4Prefixes = []; $dataCached = $memcache->get($keyCached); if (is_array($dataCached)) { # if the data is cached, even if it is empty, use the empty array $v4Prefixes = $dataCached; } else { $response = Tools::generalCurlWrapper("https://developers.google.com/static/search/apis/ipranges/googlebot.json", "GET"); $response = json_decode($response, true); $v4Prefixes = array_column((array) $response["prefixes"], "ipv4Prefix"); $ipsCleaned = []; foreach ($v4Prefixes as $ip) { // Get the Ip data, before slash there is the IP after the times to increase $ipData = explode('/', $ip); $increase = $ipData[1]; $ipsCleaned[] = $ipData[0]; // Increase IP (Last number) for ($i = 1; $i <= $increase; $i++) { $ipParts = explode('.', end($ipsCleaned)); $ipLastNumber = array_pop($ipParts) + 1; $ipsCleaned[] = implode(".", $ipParts) . ".{$ipLastNumber}"; } } $memcache->set($keyCached, (array) $ipsCleaned, 43200); # keep the list for 12 hours } # if the current IP address is part of the Google IP list, mark it in UET as google if(in_array($_SERVER["REMOTE_ADDR"], (array) $v4Prefixes)){ $event["is_google_ip"] = 1; } return $event; } /** * We store some information in the GLOBALS about the jobseeker/user that we wish to append to all UET events at click level * This section used to live inside the buildMeta method, but is now a standalone method to accommodate the churn flow * @param array $event Existing event information that we will append more columns to * @return array */ private static function setFieldsFromGlobals($event) { # flag to check whether the user is logged in at the moment of an event $isUser = 0; # adding = 1 to all user-create events if ($event['_event'] == 'user-create') { $isUser = 1; } # we will take something out of the neuvoo playbook and begin to store the date that the user created # their account in UET as well. This is extremely similar to how we store the email_date_in for SMS and email # that we then use to calculate the user lifetime value on neuvoo. if(!empty($GLOBALS['jobseeker']['date_created'])){ $isUser = 1; $event["user_id"] = $GLOBALS['jobseeker']['id']; $event["registration_date"] = $GLOBALS['jobseeker']['date_created']; $event["user_registration_source"] = (string) $GLOBALS['jobseeker']['registration_source']; $event["user_registration_soc_code"] = (string) $GLOBALS['jobseeker']['registration_soc_code']; $event["user_registration_soc_minor_group"] = (string) $GLOBALS['jobseeker']['registration_soc_minor_group']; $event["user_registration_soc_major_group"] = (string) $GLOBALS['jobseeker']['registration_soc_major_group']; $event["user_registration_source_sub"] = (string) $GLOBALS['jobseeker']['registration_source_sub']; $event["user_conversion_test"] = (string) $GLOBALS['jobseeker']['conversion_test']; if (isset($GLOBALS['jobseeker']['sms_active'])) { $event["user_sms_active"] = (string) $GLOBALS['jobseeker']['sms_active']; } if (isset($GLOBALS['jobseeker']['og_date_created'])) { $event["user_og_date_created"] = (string) $GLOBALS['jobseeker']['og_date_created']; } if (isset($GLOBALS['jobseeker']['user_strategy'])) { $event["user_strategy"] = (string) $GLOBALS['jobseeker']['user_strategy']; } if ($GLOBALS['jobseeker']['user_has_cv'] === 1) { $event["user_has_cv"] = 1; } } if(!empty($GLOBALS['remote_jobseeker']['date_created'])){ $isUser = 1; $event["user_id"] = $GLOBALS['remote_jobseeker']['id']; $event["registration_date"] = $GLOBALS['remote_jobseeker']['date_created']; $event["user_registration_source"] = (string) $GLOBALS['remote_jobseeker']['registration_source']; $event["user_registration_soc_code"] = (string) $GLOBALS['remote_jobseeker']['registration_soc_code']; $event["user_registration_soc_minor_group"] = (string) $GLOBALS['remote_jobseeker']['registration_soc_minor_group']; $event["user_registration_soc_major_group"] = (string) $GLOBALS['remote_jobseeker']['registration_soc_major_group']; $event["user_registration_source_sub"] = (string) $GLOBALS['remote_jobseeker']['registration_source_sub']; $event["user_conversion_test"] = (string) $GLOBALS['remote_jobseeker']['conversion_test']; if (isset($GLOBALS['remote_jobseeker']['sms_active'])) { $event["user_sms_active"] = (string) $GLOBALS['remote_jobseeker']['sms_active']; } if (isset($GLOBALS['remote_jobseeker']['og_date_created'])) { $event["user_og_date_created"] = (string) $GLOBALS['remote_jobseeker']['og_date_created']; } if (isset($GLOBALS['remote_jobseeker']['user_strategy'])) { $event["user_strategy"] = (string) $GLOBALS['remote_jobseeker']['user_strategy']; } if ($GLOBALS['remote_jobseeker']['user_has_cv'] === 1) { $event["user_has_cv"] = 1; } } $event["is_user"] = $isUser; return $event; } /** * creates a uniq ID from ip|userAgent to track requests coming from the "assumed" same client * @return false|mixed|string */ public static function getUUID() : string { if ($_COOKIE["uet_uuid"] != "") { return $_COOKIE["uet_uuid"]; } else { $uuid = substr(sha1($_SERVER["REMOTE_ADDR"] . $_SERVER["HTTP_USER_AGENT"]), 0, 10); Privacy::setCookie("uet_uuid", $uuid, time() + (3600 * 24 * 90)); $_COOKIE["uet_uuid"] = $uuid; return $uuid; } } /** * New implementation approach for UUID generation * Current version: 1 * @return string */ public static function genNUUID() : string { if ($_COOKIE["uet_nuuid"] != "") { return $_COOKIE["uet_nuuid"]; } else { $uuid = hash('sha256', $_SERVER["REMOTE_ADDR"] . $_SERVER["HTTP_USER_AGENT"]); Privacy::setCookie("uet_nuuid", $uuid, time() + (3600 * 24 * 180)); $_COOKIE["uet_nuuid"] = $uuid; return $uuid; } } /** * this function enables the tracking of user source trough a cookie */ private static function trackSource() : void { if(isset($_GET["sa_key"])){ $_SESSION["uet_sa_key"] = $_GET["sa_key"]; } if(isset($_GET["sa_group"])){ $_SESSION["uet_sa_group"] = $_GET["sa_group"]; } if(isset($_GET["sa_subgroup"])){ $_SESSION["uet_sa_subgroup"] = $_GET["sa_subgroup"]; } if(isset($_GET["sa_company"])){ $_SESSION["uet_sa_company"] = $_GET["sa_company"]; } # convert the get_utm source into a source, since it is a traffic avernue for us if(isset($_GET['utm_source']) && $_GET['utm_source'] == 'google_jobs_apply'){ $_GET['source'] = 'google_jobs_apply'; } # A source can only be set, if the user comes from OUTSIDE talent.com website # QUESTION Am I comming from outside talent.com ? (empty referer OR not talent.com) if ( !empty($_GET["source"]) || !isset($_SERVER["HTTP_REFERER"]) || !preg_match("#talent\.com#siu", $_SERVER["HTTP_REFERER"]) ) { preg_match("#://([^/]+)#siu", $_SERVER["HTTP_REFERER"], $match); # for instance www.google.com $refDomain = $match[1]; $_COOKIE["uet_referer"] = $refDomain; if($refDomain){ $_COOKIE["uet_source"] = 'referer'; } ################################################################################ /** * We are migrating away from using cookies for user tracking! * Instead, everything will be using memCache via a modified PHP SESSION mechanism * If you need to track some GET parameter of a user for 1 hour, use $_SESSION the exact way you would have used cookie! * * Note 2020-11-09-Stas: * This is the first test, so I am leaving cookies in still to make sure my data lines up with cookies 1-to-1! */ $_SESSION["uet_referer"] = $refDomain; if($refDomain) { $_SESSION["uet_source"] = 'referer'; # if the user comes from neuvoo, well it's not a referer it's a neuvoo redirect if(preg_match("#neuvoo#siu", $refDomain)) { $_SESSION['uet_source'] = 'neuvoo_redirect'; $_COOKIE['uet_source'] = 'neuvoo_redirect'; } } # If the referer, SESSION & get sources are all null # We know that this was a direct search for our website via its URL if($_SESSION["uet_source"] == '' && $_GET["source"] == ''){ $_SESSION["uet_source"] = 'direct'; } # this means we are inside here because referer and source was empty # the original condition checks whether the referer does not equal talent... which... # it won't if its empty if($_SERVER['HTTP_REFERER'] == '' && $_GET['source'] == '' && $_SESSION['uet_source'] == 'revived'){ $_SESSION['uet_source'] = 'direct'; $_COOKIE["uet_source"] = 'direct'; } if($_GET["source"] != ""){ $_SESSION["uet_source"] = $_GET["source"]; } # we need the same logic for the publisher as for the cookie! if($_GET["publisher"] != ""){ $_SESSION["uet_publisher"] = $_GET["publisher"]; } ################################################################################ # If the referer, cookie & get sources are all null # We know that this was a direct search for our website via its URL if($_COOKIE["uet_source"] == '' && $_GET["source"] == ''){ $_COOKIE["uet_source"] = 'direct'; } # if $_GET["source"] is set... then use that thing if($_GET["source"] != ""){ $_COOKIE["uet_source"] = $_GET["source"]; } # If there's a pbc_id we need to store it in the cookie for partner postback api if ($_GET["pbc_id"] != "") { $_SESSION["uet_pbc_id"] = $_GET["pbc_id"]; } # If there's a click_id we need to store it in the cookie for partner postback api if ($_GET["click_id"] != "") { $_SESSION["uet_click_id"] = $_GET["click_id"]; } # Set the cookie if($_COOKIE["uet_source"]!= ""){ Privacy::setCookie("uet_source", $_COOKIE["uet_source"], time() + 3600); } # we need the same logic for the publisher as for the cookie! if($_GET["publisher"] != ""){ $_COOKIE["uet_publisher"] = $_GET["publisher"]; } # Set the cookie if($_COOKIE["uet_publisher"]!= ""){ Privacy::setCookie("uet_publisher", $_COOKIE["uet_publisher"], time() + 3600); } } /** * If there is no GET Parameter & nothing in the COOKIE/SESSION NOR GET && the refer is talent * ... it means the user had our tab open and came back to it and continued looking. For some reason this seems to hapen quite often, so why not add it? */ if (empty($_GET["source"]) && empty($_SESSION['uet_source']) && empty($_COOKIE['uet_source']) && isset($_SERVER["HTTP_REFERER"]) && preg_match("#talent\.com#siu", $_SERVER["HTTP_REFERER"]) ) { if (Uet::getMobileDevice() != 'desktop') { $_SESSION['uet_source'] = 'mobile_revived'; $_COOKIE['uet_source'] = 'mobile_revived'; Privacy::setCookie("uet_source", 'mobile_revived', time() + 3600); } else { $_SESSION['uet_source'] = 'revived'; $_COOKIE['uet_source'] = 'revived'; Privacy::setCookie("uet_source", 'revived', time() + 3600); } } # Determine the type of source (or medium of traffic acquisition) the particular user has attributed to them # examples include: partner, direct, social, bulk, etc if (!empty($_SESSION['uet_source'])) { $_SESSION['uet_medium'] = self::determineSourceMedium($_SESSION['uet_source'], 'session', $_SESSION["uet_context"]); } # do the same if an actual get source is present and add a get_medium field! if (!empty($_GET['source'])) { $_GET['medium'] = self::determineSourceMedium($_GET['source'] , 'get', $_GET["context"]); } # unset the oapply for new source when the user comes from outside talent website if( !empty($_GET["source"]) && ( !isset($_SERVER["HTTP_REFERER"]) || !preg_match("#talent\.com#siu", $_SERVER["HTTP_REFERER"]) ) ) { unset($_SESSION["uet_oapply"]); } # turn oApply into a cache source parameter if(!empty($_GET["oapply"])){ $_SESSION["uet_oapply"] = $_GET["oapply"]; } } /** * Context is an identifier that shows the last internal page a user was on before committing this event * It is much easier to parse the URL structure and determine the page someone came from, than asking every developer to correcty set the context themselves. * This method does exactly that, it parses the REFERER and determines where the user is coming from * @param $fullEventName string the full name of the event (jobredirect, pageview_home, etc) */ private static function trackContext($fullEventName) { if(isset($_GET['context'])) { return; } # Whitelist of events to correct the context for if it is missing # At first, I do not want to impliment this function for every event. $eventsToCorrectFor = [ 'pageview_serp', 'pageview_home', 'jobredirect', 'intentofapply', 'user-create', 'jobSeekerPopupView', 'pageview_salarySearch', 'pageview_taxCal', 'pageview_talentpedia', 'pageview_converter' ]; # check whether the current event should be corrected if (in_array($fullEventName, $eventsToCorrectFor)) { if (empty($_SERVER['HTTP_REFERER'])) { # if there is no referer, the person opened a new tab and visited the page directly $_GET['context'] = "external_direct"; } elseif (preg_match("/talent.com/", $_SERVER["HTTP_REFERER"])) { # if the refer contains talent.com, we know that the person navigate to this page from another page on our own site # this is exactly what we wish to fix with the context_corrector function # split the referer by talent.com to get the folder/path & query string structure $splitReferer = explode("talent.com", $_SERVER["HTTP_REFERER"]); $refererUriString = $splitReferer[1]; if ( preg_match("/^\/?([a-z]{2})?\/?\/$/", $refererUriString) || preg_match("/^\/?([a-z]{2})?\/?\/\?/", $refererUriString) ) { # if file path is "/" (or empty), this event came from the main home landing page # the if is to check whether there is a query string after that $_GET['context'] = 'home'; } elseif (preg_match("/^\/?([a-z]{2})?\/?\/view/", $refererUriString)) { # if file path is "view", this event came from the whitepage if ($_GET["context"] == '') { $_GET['context'] = 'whitepage'; } } elseif (preg_match("/^\/?([a-z]{2})?\/?\/jobs/", $refererUriString)) { # if file path is "jobs", this event came from the jobs listing page $_GET['context'] = 'serp'; } elseif (preg_match("/^\/?([a-z]{2})?\/?\/salary/", $refererUriString)) { # if file path is "jobs", this event came from the jobs listing page $_GET['context'] = 'salary'; } elseif (preg_match("/^\/?([a-z]{2})?\/?\/tax-calculator/", $refererUriString)) { # if file path is "jobs", this event came from the jobs listing page $_GET['context'] = 'taxCal'; }elseif (preg_match("/^\/?([a-z]{2})?\/?\/talentpedia/", $refererUriString)) { # if file path is "talentpedia", this event came from the talentpedia page $_GET['context'] = 'talentpedia'; }elseif (preg_match("/^\/?([a-z]{2})?\/?\/convert/", $refererUriString)) { # if file path is "convert", this event came from the convert page $_GET['context'] = 'converter'; } } elseif (!preg_match("/talent.com/", $_SERVER["HTTP_REFERER"])) { # if the referer does not contain talent.com, it means that they came from somewhere else. # instead of checking the context in this case, the user should be checking either the cache_source, or cache_referer $_GET['context'] = 'external_source'; } } } /** * Determine the Medium of a Source * @param string $source * @return string */ public static function determineSourceMedium($source, $type, $context = "") { if( $source == 'neuvoo-email' || $source == 'neuvoo-email-user' || $source == 'email' || $source == 'talent-email' || $source == 'sam-talent-email' || $source == 'sam-neuvoo-email' ){ $medium = 'email'; if ($context == "email-monetize" || preg_match("/whitepage/",$context) || $context == 'popupback') { $medium = 'email-monetize'; } } elseif($source == 'revived'){ $medium = 'revived'; } elseif($source == 'mobile_revived'){ $medium = 'mobile_revived'; } elseif($source == 'referer' || $source == 'neuvoo_redirect'){ $medium = 'referer'; } elseif($source == 'direct'){ $medium = 'direct'; } elseif($source == 'easysearch'){ $medium = 'easysearch'; } elseif($source == 'neuvoosearch'){ $medium = 'neuvoosearch'; } elseif($source == 'serp'){ $medium = 'serp'; } elseif($source == 'sms' || $source == 'sms_organic'){ $medium = 'sms'; } elseif($source == 'whatsapp'){ $medium = 'whatsapp'; } elseif($source == 'api'){ $medium = 'api'; if ($context == "api-monetize" || preg_match("/whitepage/",$context) || $context == 'popupback') { $medium = 'api-monetize'; } } elseif(preg_match('/bulk/', $source) || $source == 'glassdoor_4'){ $medium = 'bulk-monetize'; } elseif(preg_match('/dtl/', $source)){ $medium = 'dtl'; } elseif(preg_match('/linkedin/', $source)){ $medium = 'linkedin'; } elseif($source == 'adwords' || $source == 'google_jobs_apply' || $source == 'facebook_ads' || $source == "bing_ads" || $source == 'gfj' || $source == 'nextdoor') { $medium = 'social'; } elseif($source == ''){ $medium = 'null'; } elseif($source == 'null' && $type == 'get'){ $medium = 'null'; } else { $medium = 'partner'; } return $medium; } /** * list of allowed get parameters * to prevent a field explosion, only allow certain getFields from being indexed * @return mixed */ private static function allowedGetFields() : array { $a["by"] = 'string'; $a["chnl"] = 'string'; $a["chnl1"] = 'string'; $a["chnl2"] = 'string'; $a["chnl3"] = 'string'; $a["click"] = 'string'; $a["falcon"] = 'int'; $a["company"] = 'string'; $a["country"] = 'string'; $a["date"] = 'string'; $a["lc"] = 'string'; //new field with geo_city value (only in the US) $a["remote"] = 'string'; //new field with remote jobs only (only in the US) $a["duc"] = 'int'; //lots of injections, I think its binary $a["e"] = 'int'; $a["ed"] = 'string'; $a["email"] = 'string'; //whitespaces getting passed in $a["email_source"] = 'string'; $a["f"] = 'string'; $a["field"] = 'string'; $a["from"] = 'string'; $a["gclid"] = 'string'; $a["id"] = 'string'; $a["ip"] = 'string'; $a["jipid"] = 'int'; $a["jpos"] = 'int'; $a["jp"] = 'int'; $a["k"] = 'string'; $a["l"] = 'string'; $a["lang"] = 'string'; $a["language"] = 'string'; // same thing as above but only used for email-alert + intent-of-apply $a["lng"] = 'string'; // i have no idea... only 44 results (also the same as language...) $a["location"] = 'string'; //replica of get_l $a["keyword"] = 'string'; //replica of get_k $a["md"] = 'string'; $a["mixgroup"] = 'string'; $a["msc"] = 'string'; //i do not know $a["o"] = 'int'; //lots of injections, guessing that it should be an int $a["p"] = 'int'; $a["pagetype"] = 'string'; $a["pag"] = 'int'; $a["pid"] = 'string'; //input not cleaned $a["pos"] = 'int'; //how is this different from j-pos? $a["publisher"] = 'string'; $a["rqid"] = 'string'; // API request ID, a hexadecimal string of 24 symbols $a["pc"] = 'string'; // placement from the API, See SearchApi::PLACEMENTS for possible values $a["puid"] = 'string'; $a["apply_type"] = 'string'; $a["job_type"] = 'string'; $a["r"] = 'int'; //in-correctly set up $a["radius"] = 'int'; $a["ref"] = 'string'; //a url.. not many cases of this.. $a["refid"] = 'string'; $a["sc"] = 'string'; //id or 'mail' $a["search"] = 'string'; //shitty $a["search_source"] = 'string'; $a["source"] = 'string'; $a["source_type"] = 'string'; //somehow equivalent to company type $a["splitab"] = 'int'; $a["test"] = 'int'; //not sure about this.... $a["tgroup"] = 'string'; $a["tld"] = 'string'; // very few cases of this.. $a["utm_medium"] = 'string'; $a["utm_source"] = 'string'; $a["utm_campaign"] = 'string'; $a["v"] = 'int'; //my guess is that it should be an int but it is not correctly set up $a["is_category"] = 'string'; $a["oapply"] = 'string'; $a["current_group"] = 'string'; $a["domain"] = 'string'; $a["abt"] = 'string'; $a["fbg_click_id"] = 'string'; $a["nb"] = 'string'; //true/false $a["subid"] = 'string'; $a["email_status"] = 'string'; //true/false $a["user_group"] = 'string'; $a["sms_send_range"] = 'int'; $a["from_id"] = 'string'; $a["from_empcode"] = 'string'; $a["from_campaign"] = 'string'; #$a["from_warning"] = 'string'; We're no longer using it because we'll take care of decoding in 3.1 #$a["facebook_adset"] = 1; //no instances $a["newjob"] = 'int'; $a["empname"] = 'string'; $a["empcode"] = 'string'; $a["fb"] = 'int'; //lots of injections, have to guess that it is int $a["ss"] = 'int'; $a["pbc_id"] = 'string'; $a["cid"] = 'string'; //incorrectly set up but with 1M+ instances since feb $a["userid"] = 'string'; $a["user_id"] = 'string'; //pretty sure this is a replica of the one above $a["ecs"] = 'string'; #$a["ecjt"] = 1; //never used $a["scs"] = 'string'; $a["scjt"] = 'string'; # sms cookie job tag $a["tl1"] = 'string'; $a["tl2"] = 'string'; $a["tl3"] = 'string'; $a["tl4"] = 'string'; $a["tl5"] = 'string'; $a["tlf"] = 'string'; //yes / no $a["wc_email"] = 'string'; $a["email_category"]= 'string'; $a["send_group"] = 'string'; $a["los"] = 'int'; // linkedin organic strategy param $a["sq"] = 'int'; // seed quality #talent email trackers $a["templateid"] = 'string'; $a["emailid"] = 'string'; $a["campaign_name"] = 'string'; $a["group_name"] = 'string'; $a["test_version"] = 'string'; $a["to_domain"] = 'string'; $a["isp_group"] = 'string'; $a["is_coreg"] = 'string'; $a["si"] = 'string'; $a["sunset"] = 'string'; $a["retargeting_count"] = 'string'; $a["triggerid"] = 'string'; $a["email_number"] = 'string'; $a["event_name"] = 'string'; $a["js_tag"] = 'string'; $a["rt_campaign_name"] = 'string'; $a["rt_template_id"] = 'string'; $a["rt_group_name"] = 'string'; $a["rt_test_version"] = 'string'; $a["page"] = 'string'; # Campaign Management System trackers $a["template_id"] = 'string'; $a["campaign_id"] = 'string'; $a["segment_id"] = 'string'; $a["notification_id"] = 'string'; $a["tier"] = 'string'; # UET Mobile app parameters $a["pushnotification_type"] = 'string'; $a["user_token"] = 'string'; $a["app_source"] = 'string'; $a["app_click_source"] = 'string'; $a["app_ab_test_notification_ping_v01"] = 'string'; # come to a naming consensus on this $a["sal-job"] = 'int'; $a["app-ab-applicants-v01"] = 'string'; #search funnel tracker $a['testid'] = 'string'; $a['eligible'] = 'string'; $a['context'] = 'string'; $a['initiator'] = 'string'; $a['exp'] = 'string'; // storing jobsense expansion $a['family'] = 'string'; $a['is_gl'] = 'int'; // Store keyword search categorization $a['is_wfh'] = 'int'; // Store keyword search categorization $a['query'] = 'int'; // Store the job source inside sonic search $a['sdb'] = 'int'; // Store the job source sonic server number $a["k_word"] = 1; $a["medium"] = 'string'; # display adsense, used in DTL for now 1: show 0: not show $a["dpa"] = 'string'; $a["tracking_abtest"] = 'string'; // sms a/b test tracker # flag to know why the traffic was moved from neuvoo to talent $a["migrate_strategy"] = 'string'; # google keyword/group $a["sa_key"] = 'string'; $a["sa_group"] = 'string'; $a["sa_subgroup"] = 'string'; $a["sa_company"] = 'string'; $a["obclid"] = 'string'; # Profile pages $a["job_alert_action"] = 'string'; $a["job_alert_set"] = 'string'; $a["job_alert_number"] = 'int'; $a["job_recommendation"] = 'string'; $a["job_alert"] = 'string'; $a["marketing_communication"] = 'string'; $a["search_id"] = 'string'; $a["force_lab"] = 'string'; #diverted traffic from landing pages $a["dtf"] = 'string'; return $a; } /** * list of allowed cookie parameters * to prevent a field explosion, only allow certain cookieFields from being indexed * @return mixed */ private static function allowedCacheFields() : array { # source and context tracking! $a['uet_origin'] = 1; $a['uet_subid'] = 1; $a["uet_source"] = 1; $a["uet_publisher"] = 1; $a["uet_referer"] = 1; $a["uet_medium"] = 1; $a["uet_oapply"] = 1; $a["uet_context"] = 1; $a["uet_utm_source"] = 1; $a["uet_utm_medium"] = 1; $a["uet_utm_campaign"] = 1; $a["uet_easysearch"] = 1; # 3rd party token trackers $a["uet_gclid"] = 1; $a["uet_fbclid"] = 1; $a["uet_msclkid"] = 1; $a["uet_obclid"] = 1; #allow this to be a dynamic session $a["uet_template_id"] = 1; $a["uet_sam_virtual_name"] = 1; $a["uet_sam_send_way"] = 1; # For A/B testing $a["uet_user_group"] = 1; $a["uet_group_name"] = 1; $a["uet_test_version"] = 1; # new ABCore tracking cookies (session cache) $a["uet_test_name"] = 1; $a["uet_test_group"] = 1; $a["uet_soc_major_group"] = 1; $a["uet_soc_minor_group"] = 1; $a["uet_job_empcode"] = 1; $a["uet_job_apply_type"] = 1; // set MVT cookies $a["uet_mvt_segment_hash"] = 1; $a["uet_mvt_touchpoint_name"] = 1; // partner postback variables $a["uet_pbc_id"] = 1; $a["uet_click_id"] = 1; # will be used for tracking applications after a click was made $a["uet_application_session"] = 1; $a["uet_email_date_sent"] = 1; $a["uet_testid"] = 1; $a["uet_internal_traffic"] = 1; $a["uet_sa_key"] = 1; $a["uet_sa_group"] = 1; $a["uet_sa_subgroup"] = 1; $a["uet_sa_company"] = 1; $a["uet_search_id"] = 1; $a["uet_strategy"] = 1; # seed quality $a["uet_sq"] = 1; $a["uet_partner_type"] = 1; $a["uet_threshold_met"] = 1; # product param $a["uet_product"] = 1; #divered traffic from lading pages $a["uet_dtf"] = 1; #user landed in whitepage due to budget issues $a["uet_whitepage_cadc"] = 1; return $a; } /** * list of allowed cookie parameters * to prevent a field explosion, only allow certain cookieFields from being indexed * @return mixed */ private static function allowedCookieFields() : array { $a["uet_source"] = 1; $a["uet_publisher"] = 1; return $a; } /** * list of allowed cookie parameters * to prevent a field explosion, only allow certain cookieFields from being indexed * @return mixed */ private static function allowedArrayFields() : array { $a["job_attribute_job_type"] = 1; $a["filter_name"] = 1; $a["filter_value"] = 1; $a["search_filters"] = 1; $a["job_attribute_jobtypes"] = 1; $a["payload"] = 1; return $a; } /** * Returns the mobile device that is currently being used * * @return mixed $device Returns the mobile device and if none, returns false * @access public */ public static function getMobileDevice() : string { preg_match(self::$mobileDevices, strtolower($_SERVER['HTTP_USER_AGENT']), $matches); $device = (isset($matches[0]) && !empty($matches[0])) ? $matches[0] : 'desktop'; return $device; } /** * This will return the pixel to update the information on our Google Analytics dashboard * @return string */ public static function GoogleAnalyticsPixelTracker($addAlt = "") : string{ # Google Analytics Tracking Pixel $param["r"] = $_SERVER["HTTP_REFERER"] ?? NULL; $uri = http_build_query($param); # Add alt if needed to improve accessibility ranking. $alt = ""; if ($addAlt != "") { $alt = "alt='{$addAlt}'"; } $trackingPixel = ""; return $trackingPixel; } /** * Return the valid methods that can be created in apply flow * @return array * @throws Exception */ public static function getAllowedApplyEvents() { $allowedEvents = array( "apply" => 1, "apply_error" => 1, "apply_exit" => 1, "apply_popup_view" => 1, "apply_redirect" => 1, "apply_submission" => 1, "apply_otp_submission" => 1, "serp_job_card_click" => 1, "cpa_application" => 1, "cpa_ioa" => 1, "serp_feedback_click" => 1, "serp_feedback_display" => 1, "serp_close_description" => 1, "testTalentEvent1" => 1, "intentofapply" => 1, "save_job" => 1, "unsave_job" => 1, "intent_of_save" => 1, "job_search" => 1, "pageview" => 1, "apply_process" => 1, "apply_submit" => 1, ); return Api::return_result($allowedEvents); } /** * Handler of apply events creation * Validate the mandatory params for the event creation and add all the required field-values to the event * @param string $eventType Name of event * @param string $seekerIdOrEmail Id or Email of Seeker that generate the event * @param string $jobId JobID where the event was generated * @param string $country Country of the job * @param string|array $extraParams Another params to add to the event * @return array * @throws Exception */ public function registerApplyEvent($eventType, $seekerIdOrEmail = '', $jobId = '', $country = '', $extraParams = '') { if (!self::getAllowedApplyEvents()["payload"][$eventType]) { return Api::return_error("internal_error", "event $eventType not allowed"); } $applicationClass = new Application(); if ($jobId && $country) { #Get information of job $jobInfo = $applicationClass->getJobApplicationCached($jobId, $country); if ($jobInfo["result"] == "fail") { return $jobInfo; } $jobInfo = $jobInfo["payload"]; } #Get the userID, the user can not be detected at the first event of apply process #Because the user is not logged in and has not entered his email $seekerInfo = $applicationClass->getSeekerIdFromTalent($seekerIdOrEmail); if ($seekerInfo["result"] == "fail") { return $seekerInfo; } $seekerId = $seekerInfo["payload"]; # Parameters will include in the event $eventParams = $this->loadApplyEventValues($seekerInfo, $jobInfo, $applicationClass)["payload"]; if ($extraParams) { if (!is_array($extraParams)) { #extraParams can be a JSON, we decode it for add it in the array $extraParams = json_decode($extraParams, true); if (json_last_error() != JSON_ERROR_NONE) { return Api::return_error("wrong format for extra_params"); } } if ($eventType == "apply_process" && ($extraParams['step'] == 'personal_info' || $extraParams['step'] == 'applyQuestions' || $extraParams['step'] == 'verify_email' || $extraParams['step'] == 'review') ) { $panelsInfo = $applicationClass->getNumberAndPanelIdsForAnApplyProcess( $jobInfo['_id'], $jobInfo["geo_country"] ); $extraParams['number_steps'] = $panelsInfo['payload']['count']; $stepInPanel = self::searchStepEquivalentInApplyPanel( $extraParams['step'] ); if ($stepInPanel && $panelsInfo['result'] == "ok") { $extraParams['step_number'] = (intval( array_search( $stepInPanel, $panelsInfo['payload']['ids'] ) ) + 1); } } foreach ($extraParams as $eventParamName => $eventParamValue) { #Add different values to the event $eventParamValue = trim($eventParamValue); if ($eventParamName && ($eventParamValue || $eventParamValue === '0' || $eventParamValue === 0 || $eventParamValue === false)) { $eventParams[$eventParamName] = ($eventParamName !== "questions_panel") ? $eventParamValue : $this->getPanelNumberForApplyEvents($eventParamValue, $jobInfo, $seekerId)["payload"]; } } } #Create the event $uetApplyEventResponse = self::registerEvent($eventType, $eventParams); /** * Essentially, if a user lands on the white-page and creates an account to apply for the specific job, * They do not actually have a job_click event, since at the moment they clicked the job they weren't a job-seeker yet * This little block of code attempts to append a job_click event based on the click that lead to the application * To allow us to start emailing the users, since in all honesty, they are the most active and best users since * they are actively applyind on jobs */ if ($eventType == "apply") { $dataExploded = explode("\n", $uetApplyEventResponse['meta']['content']); $uetData = json_decode($dataExploded[2], true); $buildEventData = self::buildJobSeekerDataArray($uetData); if ($buildEventData["result"] == "ok") { (new JobSeeker2())->appendJobClick( $seekerId, $buildEventData["payload"] ); } # Send the application information to the client if the account has a related webhook tag (new Tracker())->executeWebhookTag( $jobInfo['empcode'], $jobInfo['_id'], $jobInfo['reqid'], $jobInfo['geo_country'], $jobInfo['client_tag'], $jobInfo['link'] ); } return Api::return_result("success"); } private static function searchStepEquivalentInApplyPanel($step) { return match ($step) { "personal_info" => 'apply_standard_questions', "verify_email" => 'apply_login', "review" => 'apply_review', default => false }; } private function loadApplyEventValues($user, $job, $applicationClass) { $eventParams = [ "real_user" => Tools::detectDevRepo() ? "-1" : "1", ]; if ($user) { $eventParams = array_merge( $eventParams, ["user_id" => $user["payload"]] ); } if ($job) { $eventParams = array_merge( $eventParams, ["job_id" => $job["_id"], "job_tag" => $job["tag"], "job_ppc" => $job["ppc"], "job_ppcu" => $job["ppc_u"], "job_ppc_u" => $job["ppc_u"], "job_reqid" => $job["reqid"], "job_title" => $job["title"], "job_empcode" => $job["empcode"], "job_empname" => $job["empname"], "job_feed_type" => $job["feed_type"], "job_geo_city" => $job["geo_city"], "job_location" => $job["location"], "job_datefound" => $job["datefound"], "job_talent_ppc" => $job["talent_ppc"], "apply_type" => $job["apply_type"], "job_geo_remote" => $job["geo_remote"], "country" => $job["geo_country"], "job_geo_country" => $job["geo_country"], "job_geo_region1" => $job["geo_region1"], "job_geo_region2" => $job["geo_region2"], "job_campaign_id" => $job["campaign_id"], "job_campaign_name" => $job["campaign_name"], "job_company_type" => $job["company_type"], "job_talent_ppc_u" => $job["talent_ppc_u"], "job_soc_classifier" => $job["soc_classifier"], "job_soc_minor_group" => $job["soc_minor_group"], "job_soc_major_group" => $job["soc_major_group"], "soc_major_group" => $job["soc_major_group"], "job_soc_broad_group" => $job["soc_broad_group"], "job_soc_onetsoc_code" => $job["soc_onetsoc_code"], "job_soc_matched_title" => $job["soc_matched_title"], "job_soc_detailed_group" => $job["soc_detailed_group"], "job_attribute_job_type" => $job["source_jobtype_normalized"], "easy_apply" => (!empty($job["apply_extra_questions"])) ? "no" : "yes", "job_account_id" => $applicationClass->getAppAccountCached($job["empcode"])["payload"], "job_company" => $job["company"], "job_industry" => $job["industry"], "job_campaign_objective" => $job["campaign_objective"], # "process_app_tracker" => md5($_SERVER["REMOTE_ADDR"] . $job["_id"]) ]); } return Api::return_result($eventParams); } /** * Assign the panel number according to the position in which it was displayed during the application process * @param string $questionPanel name that identify the pop-up panel * @param string $seekerID Id of Seeker that generate the event * @param array $job Job info * @return array Array with the panel-number in the payload * @throws Exception */ private function getPanelNumberForApplyEvents($questionPanel, $job, $seekerID) { if (!$questionPanel) { # Some events have not questionPanel so, panel number will be empty return Api::return_result(""); } # Create a key to store if the user was logged in $userFlowKeyDL = "ApplyEventUserFlowDL-" . $job["_id"] . Uet::getUUID(); $userFlowKeyOTP = "ApplyEventUserFlowOTP-" . $job["_id"] . Uet::getUUID(); # By the moment, the job only have a standard question panel $panelNumber = 0; if ($questionPanel == "distant-location") { Cache::elasticCacheSet($userFlowKeyDL, true, 1800); return Api::return_result("{$panelNumber}"); } elseif (preg_match('/[a-z]+\-[0-9]+/', $questionPanel)) { # Get the panel number Ex: For panel-9 we get => 9 $panelNumber += intval(Cache::elasticCacheGet($userFlowKeyDL)); $panelNumber += substr($questionPanel, strripos($questionPanel, '-') + 1, strlen($questionPanel)); return Api::return_result($panelNumber); } if ($job["apply_extra_questions"]) { # If the job has apply_extra_questions, the total panels are counted $questions = (new EmployerQuestions())->getScreeningQuestionsByJob($job); if ($questions["result"] != "ok") { return Api::return_error("No questions"); } $questions = json_decode($questions["payload"]["questions"], true); foreach ($questions as $question) { if ($question["type"] == "pagebreak") { # A pagebreak divide two panels, so this is used to count each panel $panelNumber += 1; } } } # --- This section of code sets the panel number # --- according to the position in which it should appear during the application process. if ($questionPanel == "signInOTPEmail" || $questionPanel == "accountCreationOTP") { # The user logged in/registered from the otp panel. It is cached to know this in future events Cache::elasticCacheSet($userFlowKeyOTP, true, 1800); $panelNumber += intval(Cache::elasticCacheGet($userFlowKeyDL)); $panelNumber += 1; } elseif ($questionPanel == "panel-summary") { $panelNumber += intval(Cache::elasticCacheGet($userFlowKeyDL)); $panelNumber += intval(Cache::elasticCacheGet($userFlowKeyOTP)); $panelNumber += 1; } elseif ($questionPanel == "redirect_to_source") { $panelNumber += intval(Cache::elasticCacheGet($userFlowKeyDL)); $panelNumber += intval(Cache::elasticCacheGet($userFlowKeyOTP)); $panelNumber += 2; } elseif ($questionPanel == "panel_successful_confirmation") { # The user accept the distant location pop-up, we add +1 $panelNumber += intval(Cache::elasticCacheGet($userFlowKeyDL)); # The user crossed through OTP panel, we add +1 $panelNumber += intval(Cache::elasticCacheGet($userFlowKeyOTP)); # According to the feedcode, the user crossed through redirect_to_source panel, we add +1 more $redirect = Application::getConfigForRedirectFeedcodes($job["empcode"]); $panelNumber += intval(isset($redirect["payload"]["redirect"])); # The panel-summary number will be the number of panels in a single flow + additional panels $panelNumber += 2; Cache::elasticCacheDelete($userFlowKeyDL); Cache::elasticCacheDelete($userFlowKeyOTP); } return Api::return_result($panelNumber); } /** * This method was created by internal tools team in order to create a unique ID and store it in a session * In order to consume that ID for getting the billing information coming from jobredirect event * To store it into apply events as well throughout jobSeekerApply file. * @param $jobID the ID of the job we'll be applying to */ private static function createTalentApplyID($jobID) : void { unset($_SESSION["talentApplyID"]); $_SESSION["talentApplyID"] = md5($_SERVER["REMOTE_ADDR"] . $jobID . uniqid()); } }