@@ -210,6 +210,7 @@ ___SANDBOXED_JS_FOR_SERVER___
210210// LinkedIn Conversion API documentation: https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/conversions-api
211211
212212// Sandbox Javascript API imports
213+ // Required GTM Server-Side functions/modules
213214const sendHttpRequest = require('sendHttpRequest');
214215const getTimestampMillis = require('getTimestampMillis');
215216const getAllEventData = require('getAllEventData');
@@ -224,41 +225,39 @@ const getType = require('getType');
224225const encodeUriComponent = require('encodeUriComponent');
225226const makeTableMap = require('makeTableMap');
226227
227- // ** CONSTANTS **
228+ // Constants
228229const CONV_API_ENDPOINT = "https://api.linkedin.com/rest/conversionEvents/";
230+ const linkedin_api_version = '202503'; // LinkedIn API version (valid for 1 year from release)
229231
230- // API_version Indicates the API version that is being used. Each version is supported for 1 year following it's release.
231- const linkedin_api_version = '202410';
232-
233- // Dynamic variables
232+ // Input / Event Data Setup
234233const eventModel = getAllEventData();
235234const user_data = eventModel.user_data || { } ;
236235const user_address = user_data.address || { } ;
237236const eventDataOverride = makeOverrideTableMap(data.eventData);
238237const userIdsOverride = makeOverrideTableMap(data.userIds);
239238const userInfoOverride = makeOverrideTableMap(data.userInfo);
240239
240+ // Conversion Rule URN Construction
241241const conversion_rule_id = eventModel.conversion_rule_id || data.conversionRuleUrn;
242-
243242const conversion_rule_urn = 'urn:lla:llaPartnerConversion:' + conversion_rule_id;
244243
245- // Build conversion_event object
244+ // Build conversion event payload
245+ //Check if the conversion Happened time is provided by the user, if not then use the current timestamp.
246246const conversion_event = {
247247 conversion_happened_at: makeNumber(eventModel.conversion_happened_at || eventModel.event_time) || Math.round(getTimestampMillis()),
248248 userData: {
249249 userIds: getUserIds()
250250 }
251251};
252252
253- // Add lead ID if available
254- // Add lead ID if set in user_data
253+ // Add LinkedIn LeadGen lead ID if available
255254if (eventModel.user_data != null && eventModel.user_data.leadID != null) {
256- const leadId = eventModel.user_data.leadID;
257- const leadIdUrn = ( leadId !== " " ) ? ' urn:li:leadGenFormResponse:' + leadId : ' ' ;
258- conversion_event.userData.lead = leadIdUrn;
255+ const leadId = eventModel.user_data.leadID;
256+ const leadIdUrn = leadId !== " " ? ' urn:li:leadGenFormResponse:' + leadId : ' ' ;
257+ conversion_event.userData.lead = leadIdUrn;
259258}
260259
261- // Fetch event ID and conversion values
260+ // Conversion metadata
262261const eventID = eventDataOverride.eventId || data.eventId || eventModel.eventId || eventModel.event_id || '';
263262const conversionCurrency = eventDataOverride.currency || eventModel.currency || '';
264263const conversionAmount = eventDataOverride.amount || eventModel.value || '0';
@@ -267,67 +266,66 @@ const conversionValue = JSON.parse(data.conversionValue) || {
267266 amount: conversionAmount.toString()
268267} ;
269268
270- // Include user information if valid
271-
269+ // Add user info if available
272270const userDataInfo = getUserInfo();
273271if (userDataInfo && userDataInfo.firstName && userDataInfo.lastName) {
274- conversion_event.userData.userInfo = userDataInfo;
272+ conversion_event.userData.userInfo = userDataInfo;
275273}
276274
277- // Preparing the HTTP POST call to LinkedIn CAPI endpoint
275+ // HTTP request headers
278276const requestHeaders = {
279277 ' content-type' : ' application/json' ,
280278 ' Authorization' : ' Bearer ' + data.apiAccessToken,
281279 ' LinkedIn-Version' : linkedin_api_version
282280} ;
283281
284- var postBody = {
282+ // Construct request payload
283+ let postBody = {
285284 conversion: conversion_rule_urn,
286285 conversionHappenedAt: conversion_event.conversion_happened_at,
287286 eventId: eventID,
288287 user: conversion_event.userData
289288} ;
289+
290290if (conversionValue.currencyCode !== "" && conversionValue.amount > 0) {
291291 postBody.conversionValue = conversionValue;
292292}
293293
294- // perform validation check on presence of 1/4 of the required IDs. If at least 1 ID is present, make the API call. If no IDs are present, log the warning and no call is made
294+ // Main validation + send logic
295295if (validateUserData()) {
296296 sendConversionToLinkedIn();
297297} else {
298298 logToConsole(' No conversion event was sent to CAPI. You must set 1 out of the 4 acceptable IDs (Acxiom, Oracle, SHA256_Email, or LinkedIn_UUID) or provide firstName & lastName under eventModel.user_data.address.' );
299299}
300300
301+ // Validates presence of at least one required ID or name
301302function validateUserData() {
302- var data_is_valid_flag = false ;
303+ let data_is_valid_flag = false ;
303304 const userIDs = conversion_event.userData.userIds;
304305
305- for (var user_id of userIDs) {
306- if (user_id.idValue != " " ) {
307- // means that the idValue is present for one of the required ID feilds
306+ for (let user_id of userIDs) {
307+ if (user_id.idValue !== " " ) {
308308 data_is_valid_flag = true ;
309309 break ;
310310 }
311311 }
312- // if the flag is false, check for first name last name. if both present,
313- // flip flag to true, send the API call
314- if (data_is_valid_flag == false) {
315- if (
316- conversion_event.userData &&
317- conversion_event.userData.userInfo &&
318- conversion_event.userData.userInfo.firstName &&
319- conversion_event.userData.userInfo.lastName
320- ) {
312+
313+ if (!data_is_valid_flag) {
314+ const userInfo = conversion_event.userData.userInfo;
315+ if (userInfo && userInfo.firstName && userInfo.lastName) {
321316 data_is_valid_flag = true ;
322317 }
323318 }
319+
324320 return data_is_valid_flag;
325321}
326322
323+ // Converts array of overrides into map
327324function makeOverrideTableMap(values) {
328325 return makeTableMap(values || [], ' name' , ' value' ) || {} ;
329326}
330327
328+ // Gathers available user IDs
331329function getUserIds() {
332330 const userIds = [{
333331 idType: ' SHA256_EMAIL' ,
@@ -347,12 +345,13 @@ function getUserIds() {
347345 }
348346 ];
349347
350- return userIds.filter(( userId) => userId.idValue);
348+ return userIds.filter(userId => userId.idValue);
351349}
352350
351+ // Gets email, prioritizing hashed version
353352function getUserEmail() {
354353 const hashedEmail = getUserDataHashedEmail();
355- if (hashedEmail) return hashedEmail; // Prioritize hashed email if available
354+ if (hashedEmail) return hashedEmail;
356355
357356 return (
358357 (userIdsOverride.email ||
@@ -376,24 +375,21 @@ function getOracleMoatId() {
376375 return userIdsOverride.moatID || user_data.moatID || ' ' ;
377376}
378377
378+ // Gets LinkedIn first-party UUID
379379function getFirstPartyAdsTrackingUuid() {
380- // Check if eventModel.user_data exists before accessing its properties
381380 const eventLinkedinFirstPartyID =
382381 eventModel.user_data && eventModel.user_data.linkedinFirstPartyId ? eventModel.user_data.linkedinFirstPartyId : ' ' ;
383-
384- // Fallback: Try fetching from user properties
385382 const ga4UserPropPrefix = ' x-ga-mp2-user_properties.' ;
386383 const userLinkedinFirstPartyID = getEventData(ga4UserPropPrefix + ' linkedinFirstPartyId' ) || ' ' ;
387-
388- // Return the first available ID
389384 return eventLinkedinFirstPartyID || userLinkedinFirstPartyID || ' ' ;
390385}
391386
387+ // Fallback lookup for user information
392388function getUserFirstName() {
393389 return (
394390 userInfoOverride.firstName ||
395391 eventModel.firstName ||
396- eventModel.FirstName || // Handles possible inconsistent casing
392+ eventModel.FirstName ||
397393 eventModel.nameFirst ||
398394 eventModel.first_name ||
399395 user_data.first_name ||
@@ -406,7 +402,7 @@ function getUserLastName() {
406402 return (
407403 userInfoOverride.lastName ||
408404 eventModel.lastName ||
409- eventModel.LastName || // Handles possible inconsistent casing
405+ eventModel.LastName ||
410406 eventModel.nameLast ||
411407 eventModel.last_name ||
412408 user_data.last_name ||
@@ -418,7 +414,7 @@ function getUserLastName() {
418414function getUserJobTitle() {
419415 return (
420416 userInfoOverride.jobTitle ||
421- eventModel.jobTitle || // Handles possible inconsistent casing
417+ eventModel.jobTitle ||
422418 user_data.jobTitle ||
423419 user_data.job_title ||
424420 ' '
@@ -428,7 +424,7 @@ function getUserJobTitle() {
428424function getUserCompanyName() {
429425 return (
430426 userInfoOverride.companyName ||
431- eventModel.companyName || // Handles possible inconsistent casing
427+ eventModel.companyName ||
432428 eventModel.company_name ||
433429 user_data.companyName ||
434430 user_data.company_name ||
@@ -439,14 +435,15 @@ function getUserCompanyName() {
439435function getUserCountryCode() {
440436 return (
441437 userInfoOverride.countryCode ||
442- eventModel.countryCode || // Handles possible inconsistent casing
438+ eventModel.countryCode ||
443439 eventModel.country ||
444440 user_data.country ||
445441 user_address.country ||
446442 ' '
447443 );
448444}
449445
446+ // Builds userInfo object
450447function getUserInfo() {
451448 return {
452449 firstName: getUserFirstName(),
@@ -457,55 +454,38 @@ function getUserInfo() {
457454 } ;
458455}
459456
457+ // Check if a value is already a valid SHA256 hash
460458function isHashed(value) {
461- if (! value) {
462- return false ;
463- }
464-
459+ if (! value) return false ;
465460 return makeString(value).match(' ^[A-Fa-f0-9]{64}$' ) !== null;
466461}
467462
463+ // Hashes non-object and non-array string values if not already hashed
468464function hashData(value) {
469- if (! value) {
470- return value;
471- }
465+ if (! value) return value;
472466
473467 const type = getType(value);
474-
475- if (type === 'undefined' || value === 'undefined') {
476- return undefined;
477- }
478-
479- if (type === 'object' || type === 'array') {
480- return value;
481- }
482-
483- if (isHashed(value)) {
484- return value;
485- }
468+ if (type === ' undefined' || value === ' undefined' ) return undefined;
469+ if (type === ' object' || type === ' array' ) return undefined;
470+ if (isHashed(value)) return value;
486471
487472 value = makeString(value).trim().toLowerCase();
488-
489473 return sha256Sync(value, {
490474 outputEncoding: ' hex'
491475 } );
492476}
493477
478+ // Encodes a string for URI safety
494479function enc(data) {
495480 return encodeUriComponent(data || ' ' );
496481}
497482
498- function makeOverrideTableMap(values) {
499- return makeTableMap(values || [], ' name' , ' value' ) || {} ;
500- }
501-
483+ // Send the API request to LinkedIn
502484function sendConversionToLinkedIn() {
503- // Sending the API Call
504485 sendHttpRequest(CONV_API_ENDPOINT, {
505486 headers: requestHeaders,
506487 method: ' POST' ,
507- //timeout: 500,
508- } , JSON.stringify(postBody)).then((result) => {
488+ } , JSON.stringify(postBody)).then(result => {
509489 if (result.statusCode >= 200 && result.statusCode < 300) {
510490 data.gtmOnSuccess();
511491 } else {
@@ -514,6 +494,7 @@ function sendConversionToLinkedIn() {
514494 });
515495}
516496
497+
517498___SERVER_PERMISSIONS___
518499
519500[
0 commit comments