JavaScript best practices
JavaScript is the standard programming language of the Internet. Medallia Experience Cloud uses JavaScript (JS) to enable customization of client programs on a common software platform. With some awareness and a few pointers, writing functional, maintainable, and scalable JavaScript becomes very simple.
JavaScript best practices cover:
Resources
- Google JavaScript Style Guide — This is the de facto standard for JS style on the Internet.
- idiomatic.js — This has great insight into the philosophy of coding best practices.
- JSHint — A code style checker.
- jscomplexity — A code complexity checker. As a rough guide, cyclomatic complexity should be less than 10 at maximum, less than 5 ideally.
Best practices
Consistency
Above all else, be consistent with your JS. Code should always feel as though it were written by a single person with a single style rather than multiple developers. If you are making a minor modification to a legacy JS implementation, adopt the style used by that implementation.
Line length
Lines should typically be wrapped at 90 characters in length so that the code properly fits in the minimized JS windows. There are exceptions that are covered further below (regular expressions being the most notable).
Code style
/**
* This is called a doc-block, and allows the programmer to document his/her intent behind the function. The first sentence should always be a summary of the function's
purpose, and be relatively concise. Further clarification can be made in additional sentences or paragraphs.
* Note that doc-blocks, by convention, use the JSDoc form comment. This has historical precedent due to JavaDoc syntax.
*/
(function () {
// *** SECTION 1 ***
// Indent with one tab or 4-spaces, but never mix tabs and spaces. Tabs are slightly preferred due to the way the Medallia JS editor works.
// *** SECTION 2 ***
// Statements should always end in semicolons, even though they are sometimes not strictly required. This promotes consistency and readability.
// *** SECTION 3 ***
// Be descriptive in your variable, function, and class names. Use names that are human-readable and clear to someone who is not part of an implementation.
// Lengthier names are better ('likelihoodToRecommend' or 'ltr' instead of 'l').
var likelihoodToRecommend = seqnum(q_company_ltr_alt);
// Place comments on their own lines, and never at the end of a line.
// Constants should be capitalized. JS does not strictly enforce constants, so this is done as a convention for programmers.
var SECONDS_PER_DAY = 86400;
var PI = 3.14159;
// Object names should be CamelCased. Use objects to organize common attributes, such as AltDb entries in AltSets.
var CompanyNpsSegment = {
PROMOTER : 1,
PASSIVE : 2,
DETRACTOR : 3
};
// *** SECTION 4 ***
// Variable and function names, unless provided by the Medallia system directly, should be camelCased and highly descriptive of their purpose.
var likelihoodToRecommend = seqnum(q_company_score_ltr_alt);
function isPromoter(ltr) {
...
};
// *** SECTION 5 ***
// Prefer single-quotes (') over double-quotes (") for string literals.
var transactionAmount = '$' + e_company_transaction_amt;
// *** SECTION 6 ***
// Use spaces, braces, and multi-line spans to encourage readability. Exception: inner whitespace on condition expressions is not recommended.
if (someVariable === someValue) {
var recipients = [
'"Jane Smith" <jsmith@example.com>',
'"Robin Johnson" <rjohnson@example.com>',
'"Bryan Williams" <bwilliams@example.com>'
];
}
// *** SECTION 7 ***
// Always prefer comparing using identity (===) rather than equality (==). For example, all of the following are equal (==) and could result in
// unexpected results:
//
// - null
// - undefined
// - 0
// - '' (empty string)
// - false
//
if (q_company_score_ltr_alt === null) {
...
}
// *** SECTION 8 ***
// Reduce the number of nested levels to promote readability. This can be
accomplished by keeping the following in mind:
// Test for error cases first to avoid unnecessary processing.
// Avoid use of 'else' if a 'return' statement makes it unnecessary and addition of the 'else' does not improve code readability.
if (q_company_field !== null) {
return q_company_field;
}
if (e_company_field !== null) {
return e_company_field;
}
return null;
// *** SECTION 9 ***
// Use human-readable names rather than magic numbers to promote readability.
var CompanyYesNo = {
YES : 1,
NO : 2
};
return CompanyYesNo.YES;
// *** SECTION 10 ***
// Use the ternary operator to return simple if/else values.
return (e_company_trans_amt_txt !== null) ? e_company_trans_amt_txt : '(n/a)';
// *** SECTION 11 ***
// Use the "OR" operator pattern to set variables to a known default or return a default value. This is described in a section below.
var employmentStatus = q_company_employed_yn || e_company_employed_yn;
return q_company_email_txt || e_company_email_txt;
}());
Logic Comparisons
JavaScript supports two different logical comparisons:
- Equality (Loose equality).
- Identity (Strict equality).
| Operator | Equality Type |
|---|---|
| ==, != | Loose |
| >, >=, <, <= | Loose |
| ===, !== | Strict |
Loose equality (Equality)
Loose equality answers the question Are these two things equal in value?. Examples of equality include:
0 == 0 => true
null == 0 => true
null == '' => true
'123' == 123 => true
'abc' == 'abc' => true
null == undefined => true
false == undefined => true
true == 'a' => true
Note that in these examples, the values on the left are functionally equivalent to the values on the right. For example, the string 123 is equivalent to the integer 123. A null is equivalent to an empty string.
Loose equality requires some additional thought around whether the results of a comparison would make sense. You have to be aware of data types, behaviors of the JavaScript virtual machine, and some other nuanced aspects.
Strict Equality (Identity)
0 === 0 => true
null === 0 => false
null === '' => false
'123' === 123 => false
'abc' === 'abc' => true
null === undefined => false
false === undefined => false
true === 'a' => falseIn effect, when using strict equality, results are very easy to predict: Thing X is not exactly the same as Thing Y. It forces compliance with data types.K-field and R-field function declaration
/**
* This is called a doc-block, and allows the programmer to document his/her intent behind the function. The first sentence should always be a summary of the function's purpose, and be relatively concise. Further clarification can be made in additional sentences or paragraphs.
*/
(function () {
// Code goes here. Must have one or more *return* statements.
}());The first section is a JavaScript documentation block (doc-block) and describes the purpose of the function.-
Always start and end with parenthesis.
-
Call the function inside of the closing parenthesis.
Default Values using "OR" assignment
var value = fields.get('q_company_field_name_1');
if (value === null) {
value = fields.get('q_company_field_name_2');
if (value === null) {
value = '';
}
}
return value;var value = fields.get('q_company_field_name_1') || fields.get('q_company_field_name_2') || '';
return value;The first example above uses a nested-if statement to determine which field should be returned. The second example leverages the JavaScript OR operator (||) for cleaner code.OR operator works by returning the first value, from left to right, that evaluates (using loose equality) to a true value. The following truth table shows how the above OR expression would evaluate given different scenarios:| q_company_field_name_1 | q_company_field_name_2 | Field Used | Result |
|---|---|---|---|
| 1 | 2 | q_company_field_name_1 | 1 |
| null | 2 | q_company_field_name_2 | 2 |
| 1 | null | q_company_field_name_1 | 1 |
| null | null | '' | '' |
It is always preferable to use the OR operator to evaluate default values or simple return statements due to the improved code readability.
AltSet references
As most humans are generally not walking, talking references for AltSets, as a pattern, it is recommended to create a helpful and descriptive enumeration in your JavaScript code for any AltSet used in the code itself. This is most easily done by creating an object:
/**
* This is an example of how to describe AltSet references in a human-readable manner.
*/
(function () {
// AltSet references
var CompanyNpsSegment = {
PROMOTER : 1,
PASSIVE : 2,
DETRACTOR : 3
};
var CompanyYesNo = {
YES : 1,
NO : 2
};
// Remaining code goes here
}());This now allows for using, for example, CompanyNpsSegment.DETRACTOR in a return statement rather than the cryptic value 3.field.get will return a java.lang.String rather than an integer. To workaround this, wrap the sequence number for the AltSet references in quotes and cast the fields.get() result, as shown below:// AltSet references on Survey Engine
var CompanyYesNo = {
YES : '1',
NO : '2'
};
var someField = fields.get('q_company_somefield');
if (someField !== null && String(someField) === CompanyYesNo.YES) {
// Business logic here, omitted for the sake of the example
}
Array loops
There are two ways to loop through arrays: for and forEach(). Both have their pros and cons, as described below.
"For" loops
for loop is:for (var loopCounter = startValue; loopCondition; postLoopFollow) {
// Code to execute as part of the loop.
}You can use a structure like this to loop through an array:/**
* Reset Q-fields are the beginning of a survey.
*/
(function () {
var restaurantPlaceholders = [
'q_htl_restaurant_1_name_selected_auto',
'q_htl_restaurant_2_name_selected_auto',
'q_htl_restaurant_3_name_selected_auto'
];
for (var i = 0; i < restaurantPlaceholders.length; i++) {
var restaurantPlaceholder = restaurantPlaceholders[i];
fields.set(restaurantPlaceholder, null);
}
}());This structure is very useful if you need to break or continue based on programmatic conditions or perform other non-standard loop flows:/**
* Example of a non-standard loop flow. This is NOT a best practice for several reasons, but is shown here so you can understand how to use the structure.
*/
(function () {
var qFields = [
'q_field_1',
'q_field_2',
'q_field_3',
'q_field_4',
'q_field_5',
'q_field_6',
'q_field_7'
];
var skipQField4 = false;
for (var i = 0; i < qFields.length; i++) {
var fieldName = qFields[i];
var fieldValue = fields.get(fieldName);
if (fieldName === 'q_field_3' && fieldValue > 8) {
skipQField4 = true;
} else if (fieldName === 'q_field_4' && skipQField4) {
fields.set(fieldName, null);
continue;
}
}
}());"forEach" loops
forEach loop is:array.forEach(function (arrayElement, arrayIndex, arrayReference) {
// Code to execute as part of the loop.
});The forEach loop iterates through each element in the array and calls the function callback that you define. The callback is provided 3 parameters by the forEach mechanism: (1) the next element in the array to process, (2) the index of the element in the array, and (3) a reference to the array itself. /**
* Reset Q-fields are the beginning of a survey.
*/
(function () {
var restaurantPlaceholders = [
'q_htl_restaurant_1_name_selected_auto',
'q_htl_restaurant_2_name_selected_auto',
'q_htl_restaurant_3_name_selected_auto'
];
restaurantPlaceholders.forEach(function (restaurantPlaceholder, index, array) {
fields.set(restaurantPlaceholder, null);
});
}());The forEach mechanism can have better performance and is generally considered more self-descriptive. As such, when looping over arrays, the best practice is to use forEach, as shown in the below example from a survey element script:/**
* Setup up to three sub-records based on previously selected restaurants.
*/
(function() {
// Alt Set references from the company instance
var CompanyYesNo = {
YES : 1,
NO : 2
};
// The input Q-fields, with a selection field associated with a name field.
var restaurantFields = [
{
isSelected: 'q_htl_restaurant_1_selected_yn',
name: 'q_htl_restaurant_1_name_auto'
},
{
isSelected: 'q_htl_restaurant_2_selected_yn',
name: 'q_htl_restaurant_2_name_auto'
},
{
isSelected: 'q_htl_restaurant_3_selected_yn',
name: 'q_htl_restaurant_3_name_auto'
},
{
isSelected: 'q_htl_restaurant_4_selected_yn',
name: 'q_htl_restaurant_4_name_auto'
},
{
isSelected: 'q_htl_restaurant_5_selected_yn',
name: 'q_htl_restaurant_5_name_auto'
}
];
// The output Q-fields where the final data is stored
var restaurantPlaceholders = [
'q_htl_restaurant_1_name_selected_auto',
'q_htl_restaurant_2_name_selected_auto',
'q_htl_restaurant_3_name_selected_auto'
];
// Set all placeholder fields to null. This is used in case guest hits
// back to reselect restaurants. It will not store question answers
// when user goes back to the restaurant page.
restaurantPlaceholders.forEach(function (placeholderField, index, array) {
fields.set(placeholderField, null);
});
// Evaluate each selected/name pair to decide whether to store the
// restaurant's name to the next placeholder field.
restaurantFields.forEach(function (restaurant, index, array) {
// Only evaluate the restaurant if there is an open placeholder
if (restaurantPlaceholders.length) {
if (fields.get(restaurant.isSelected) === CompanyYesNo.YES) {
var placeholderField = restaurantPlaceholders.shift();
var restaurantName = fields.get(restaurant.name)
fields.set(placeholderField, restaurantName);
}
}
});
// Return true so the survey continues
return true;
}());Note that you can leave off unused/unneeded arguments from the callback function to the forEach, as shown in the example below:/**
* This function is used to set the restaurant names in q-fields that will
* later be used in the sub-record. It takes a UDF from the unit with each
* of the restaurants and based on the survey taker's selection, populates
* up to three q-fields.
*/
(function() {
// Alt Set references from the company instance
var CompanyYesNo = {
YES : 1,
NO : 2
};
// The input Q-fields, with a selection field associated with a name field
var restaurantFields = [
{
isSelected: 'q_htl_restaurant_1_selected_yn',
name: 'q_htl_restaurant_1_name_auto'
},
{
isSelected: 'q_htl_restaurant_2_selected_yn',
name: 'q_htl_restaurant_2_name_auto'
},
{
isSelected: 'q_htl_restaurant_3_selected_yn',
name: 'q_htl_restaurant_3_name_auto'
},
{
isSelected: 'q_htl_restaurant_4_selected_yn',
name: 'q_htl_restaurant_4_name_auto'
},
{
isSelected: 'q_htl_restaurant_5_selected_yn',
name: 'q_htl_restaurant_5_name_auto'
}
];
// The output Q-fields where the final data is stored
var restaurantPlaceholders = [
'q_htl_restaurant_1_name_selected_auto',
'q_htl_restaurant_2_name_selected_auto',
'q_htl_restaurant_3_name_selected_auto'
];
// Filter out any restaurantFields entries that have not been selected
restaurantFields.forEach(function (restaurant, index, restaurantFields) {
if (fields.get(restaurant.isSelected) !== CompanyYesNo.YES) {
restaurantFields.splice(index, 1);
}
});
// Set all placeholder fields to null. This is used in case guest hits
// back to reselect restaurants. It will not store question answers
// when user goes back to the restaurant page.
restaurantPlaceholders.forEach(function (placeholderField) {
fields.set(placeholderField, null);
});
// Assign random restaurants from the selected set to the placeholders.
restaurantPlaceholders.forEach(function (placeholderField) {
// Only evaluate if there are any remaining restaurantFields entries
if (restaurantFields.length) {
// Select a random number between 0 and restaurantFields.length.
var restaurantFieldIndex = getRandomInteger(restaurantFields.length + 1);
// Look up the restaurantFields entry at that index and remove/return it
var restaurant = restaurantFields.splice(restaurantFieldIndex, 1);
// Assign the restaurant entry to the placeholder being processed
var restaurantName = fields.get(restaurant.name);
fields.set(placeholderField, restaurantName);
}
});
// Return true so the survey continues
return true;
/**
* Returns a random integer between 0 (inclusive) and max (exclusive).
*/
function getRandomInteger(max) {
return Math.floor(Math.random() * max);
}
}());
Future-proofing
Implementations will likely change in the future as their custom engagement improves based on their use of Experience Cloud. Sometimes, those changes could break existing implementations in very subtle ways.
/**
* Convert the LTR score into the proper category (promoter/passive/detractor).
*/
(function () {
// This example is simplified to illustrate the point. Don't use it
// as a style reference!
var CompanyNpsSegment = {
PROMOTER : 1,
PASSIVE : 2,
DETRACTOR : 3
};
var LTR = seqnum(q_retailer_store_ltr_alt);
if (LTR === null) {
return null;
}
if (LTR >= 9) {
return CompanyNpsSegment.PROMOTER;
}
if (LTR >= 7) {
return CompanyNpsSegment.PASSIVE;
}
return CompanyNpsSegment.DETRACTOR;
}());When changing the AltSet for the Q-field to include a new N/A option, the new N/A option will have a sequence number of 11. Based on the logic above, N/A would be treated like a promoter (>= 9).// ...
var LTR = seqnum(q_retailer_store_ltr_alt);
if (LTR === null || LTR > 10) {
return null;
}
if (LTR >= 9) {
// ...Code complexity
Code complexity refers to the number of possible execution paths that a function can take based on the total number of different inputs. Code complexity has a direct correlation with testability, maintainability, and quality incidents. One measure of code complexity is called cyclomatic complexity, which specifically looks at the number of branches in the code as a measure of complexity. It is highly recommended to use a tool such as jshint to estimate your code's complexity.
Cyclomatic complexity should ideally be less than 5 and no greater than 10.
Regular expressions
Escape dots
var option1 = /@(medallia|partners.medallia|example).com$/;
var option2 = /@(medallia|partners\.medallia|example)\.com$/;Now consider the following test inputs:- jsmith@medallia.com
- jsmith@partners.medallia.com
- jsmith@example.com
- jsmith@partnersamedallia.com
Ideally these inputs should all be filtered out by the regular expressions, however option1 will fail to filter out the last input in the list above. This is due to how JS regular expressions use the dot (.) character.
In JS regular expressions, and unless inside of a character class sequence ([.]), dot matches any single character except line terminators (\r, \n, \u2028, and \u2029). If you want to specifically match a literal dot and not use the "special" meaning that JS assigns to the dot, then you need to escape the dot using the backslash (\) character before the dot, as done by the expression in option2.
Capture groups
Regular expressions support capture groups. A capture group is a portion of the regular expression that is surrounded by parentheses and functions both as part of the overall expression and as an expression of its own. When the regular expression with capture groups is matched, the return value is an array that contains the full portion of the string matched in the first position of the array [0], followed by the strings of any capture group matches.
Each capture group occupies a fixed position in the returned array, its position is determined by the order in which the opening parenthesis appear in the expression.
For the best possible efficiency, use the below pattern to both verify a match (based on the null check) and pull specific data out of the regular express match.
* Returns the date on which the corporate team reviewed the record.
*/
(function () {
var regexp = /[(\d{4})-(\d{2})-(\d{2})] Record reviewed by corp team/;
var matches = regexp.exec(a_comments);
// Verify that a match was found
if (matches === null) {
return null;
}
// Get the different portions from the overall match
var year = matches[1];
var month = matches[2] - 1; // Months are zero-indexed (0 = January, 11 = December)
var day = matches[3];
return new Date(year, month, day);
}());'[2017-12-22] Record reviewed by corp team', an array is returned with the following values:matches[0] = '[2017-12-22] Record reviewed by corp team';
matches[1] = '2017';
matches[2] = '12';
matches[3] = '22';Testing only
If you only need to verify that some data matches a format, avoid the use of capture groups in the regular expression and perform a simple test operation.
/**
* Returns whether the corporate team has reviewed the record.
*/
(function () {
var regexp = /[\d{4}-\d{2}-\d{2}] Record reviewed by corp team/;
return regexp.test(a_comments);
}());
Dates and Times
As a general rule, Date and Date/time objects should be represented the standard JavaScript Date object or Java Date object. Unless otherwise specified, these objects are based in the servers' local time zone (currently America/Los_Angeles).
Formatting Date/Times to Strings
Suppose you want to use a European date/time format for the survey response date (e_responsedate) and want to convert from the servers' local time zone to the client's time zone. The current best practice is to use the Joda-Time library as shown in the following code snippet. Reference our Date and Time Formatting page for further details on the format patterns shown.
/**
* Returns the response date as a string in a European format (dd mmm yyyy hh:mm:ss).
*/
(function () {
var responseDate = e_responsedate;
if (responseDate === null) {
return responseDate;
}
// Convert from java.util.Date to org.joda.time.DateTime
responseDate = date(responseDate);
// Convert to a string
var formatter = org.joda.time.format.DateTimeFormat.forPattern('dd MMM yyyy HH:mm:ss z');
return formatter.print(responseDate);
}());If you also wanted to convert the time zone, use the following:/**
* Returns the response date as a string in a European format (dd mmm yyyy hh:mm:ss).
*/
(function () {
var responseDate = e_responsedate;
if (responseDate === null) {
return responseDate;
}
// Convert from java.util.Date to org.joda.time.DateTime in the target time zone
var timeZone = org.joda.time.DateTimeZone.forID('America/Buenos_Aires');
responseDate = date(responseDate).toDateTime(timeZone);
// Convert to a string
var formatter = org.joda.time.format.DateTimeFormat.forPattern('dd MMM yyyy HH:mm:ss z');
return formatter.print(responseDate);
}());Calculating time differences between two datesThere are three different ways of representing differences in time:
Interval — An object that holds the start and end date-time, allowing for operations based around that time range.
Duration — An object that holds the exact duration in milliseconds.
Period — An object that holds a duration, defined in terms of fields (years, months, etc.).
/**
* Returns the period between two dates, formatted as a fancy string. Examples:
* 2y 48w 3h 57m 2s
* 6h 33m 5s
*/
(function () {
var date1 = e_some_date_field;
if (date1 === null) {
return null;
}
var date2 = e_some_other_date_field;
if (date2 === null) {
return null;
}
// Convert from java.util.Date to org.joda.time.DateTime
date1 = date(date1);
date2 = date(date2);
// Calculate the org.joda.time.Period between the two dates
var delta = new org.joda.time.Period(date1, date2);
// Convert to a string
return new org.joda.time.format.PeriodFormatterBuilder()
.appendYears()
.appendSuffix('y', 'y')
.appendSeparator(' ')
.appendWeeks()
.appendSuffix('w', 'w')
.appendSeparator(' ')
.appendHours()
.appendSuffix('h', 'h')
.appendSeparator(' ')
.appendMinutes()
.appendSuffix('m', 'm')
.appendSeparator(' ')
.appendSeconds()
.appendSuffix('s', 's')
.toFormatter()
.print(delta);
}());The above example shows a human-readable format. If setting up an export that will be processed by computerized systems, other formats should be considered. The ISO 8601 format specifies an internal standard for date/time interchange, for example, and can be used as shown below: ...
return org.joda.time.format.ISOPeriodFormat.standard().print(delta);
}());Calculating relative age/**
* Calculates the relative age of the survey respondent.
* WARNING: THIS K-FIELD SHOULD ALWAYS BE SET TO EXPORT ONLY TO AVOID CACHE STALENESS ISSUES.
*/
(function () {
var birthday = e_respondent_birthday
if (birthday === null) {
return null;
}
// Convert from java.util.Date to org.joda.time.DateTime
birthday = date(birthday);
// Calculate the person's age
var now = new org.joda.time.DateTime();
var delta = new org.joda.time.Period(birthday, now);
return delta.getYears();
}());
Examples
As the best way to learn best practices are to see them in action, below are several examples. All of these examples are taken from production instances, though they may have been anonymized to protect clients.
K-Field to convert LTR score to Promoter/Passive/Detractor classification
/**
* Converts from LTR values to Promoter/Passive/Detractor classification. If LTR is not available for the survey record, return 'null'.
*/
(function () {
var CompanyNpsSegment = {
PROMOTER : 1,
PASSIVE : 2,
DETRACTOR : 3
};
var likelihoodToRecommend = seqnum(q_retailer_store_ltr_alt);
if (likelihoodToRecommend === null) {
return null;
}
// Protect against modifying the LTR AltSet in the
// future to include an NA entry, for example.
if (likelihoodToRecommend > 10) {
return null;
}
if (likelihoodToRecommend >= 9) {
return CompanyNpsSegment.PROMOTER;
}
if (likelihoodToRecommend >= 7) {
return CompanyNpsSegment.PASSIVE;
}
return CompanyNpsSegment.DETRACTOR;
}());
K-Field to select email address
/**
* Select either the surveyed email address (preferred) or the client-provided email address (backup).
*/
(function () {
return q_company_email_txt || e_company_email_txt;
}());
R-Field to get the number of Invitations sent
/**
* Return the number of invitations sent.
* This involves making two cuts into the record data. The first cut determines which records were tagged as ones to invite to take a survey. The second cut determines which email invitations were actually sent.
*/
(function () {
// Array of the status codes where an invite would have been sent.
// Note that 'sent' does not imply 'received' or 'delivered
// successfully'.
var INVITE_SENT_STATUS_CODES = [
'COMPLETION_PENDING',
'COMPLETED',
'SURVEY_ENGINE_ONLY',
'EXCLUDED',
'AUTO_EXCLUDED',
'EXIT_AUTO_EXCLUDED',
'DELIVERED',
'DELIVERED_AND_REMINDED',
'DELIVERED_NO_REMINDER',
'EXPIRED',
'DELIVERY_FAILED',
'DELIVERY_BOUNCED',
'DELIVERED_REMINDER_TO_BROADCASTER',
'PARTIALLY_COMPLETED',
'PARTIALLY_COMPLETED_AND_REMINDED',
'RESET',
'INVITATION_RESENT',
'RESET_PENDING'
];
return
cube
.cut('e_survey_source', ['INVITED'])
.cut('e_status', INVITE_SENT_STATUS_CODES)
.count;
}());
R-Field to get the Click-through rate
/**
* Return the number of survey invites that resulted in a survey being started. This is an effective measure of the click-through rate. Note that click-through rate is different than the email open rate. This calculation depends on the following fields, which should be created first:
* - r_retailer__invitations_successful
*/
(function () {
var numInvitedAndStarted =
cube
.cut('e_survey_source', ['INVITED'])
.cut('a_survey_is_started', ['Yes'])
.count;
var numInvited = cube.field('r_retailer__invitations_successful');
return 100 * numInvitedAndStarted / numInvited;
}());
Regex Capture groups: Example of Receipt code validation in an Anonymous survey
/**
* Validates the receipt code provided by the user. If the receipt code is
* valid, its components are stored into various fields for later reference.
*
* Receipt code structure:
*
* S4 R3 S1 T2 Y2 T4 R1 T1 Y1 S2 C1 T5 D1 R2 S3 M2 D2 T3 M1
* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
*/
(function () {
// Alt Set references
var CompanyYesNo = {
YES : 1,
NO : 2
};
// Receipt code references
var SurveyCode = {
CORE_STORE : 1,
GENERAL : 2,
SHIPPING_PRODUCT : 3
};
// If user doesn't have receipt, sets termination flag to YES
var valid = fields.get('q_company_store_all_receipt_code_alt');
var receiptCode = fields.get('q_company_store_all_receipt_code_txt');
if (valid === '2' && receiptCode === null) {
fields.set('e_company_survey_termination_flag_yn', CompanyYesNo.YES);
return true;
}
var regex = /^(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)$/;
var receiptCodeComponents = receiptCode.match(regex);
if (!receiptCodeComponents) {
resetFields();
return false;
}
var S4 = receiptCodeComponents[1];
var R3 = receiptCodeComponents[2];
var S1 = receiptCodeComponents[3];
var T2 = receiptCodeComponents[4];
var Y2 = receiptCodeComponents[5];
var T4 = receiptCodeComponents[6];
var R1 = receiptCodeComponents[7];
var T1 = receiptCodeComponents[8];
var Y1 = receiptCodeComponents[9];
var S2 = receiptCodeComponents[10];
var C1 = receiptCodeComponents[11];
var T5 = receiptCodeComponents[12];
var D1 = receiptCodeComponents[13];
var R2 = receiptCodeComponents[14];
var S3 = receiptCodeComponents[15];
var M2 = receiptCodeComponents[16];
var D2 = receiptCodeComponents[17];
var T3 = receiptCodeComponents[18];
var M1 = receiptCodeComponents[19];
var storeNumber = S1 + S2 + S3 + S4;
var storeId = '[STORE_UNIT_ID]' + storeNumber;
var unit = fields.getAltById('q_company_store_all_store_selector_unit', storeId);
if (!unit) {
resetFields();
return false;
}
var year = parseInt(Y1 + Y2, 10) + 2000;
var month = parseInt(M1 + M2, 10);
var day = parseInt(D1 + D2, 10);
var transactionDate = new Date(year, month, day);
var now = new Date();
// Check that the date code is valid (leap year, etc.)
if (!isDateValid(transactionDate, now, year, month, day)) {
resetFields();
return false;
}
var transactionAge = msecToDays(now.getTime() - transactionDate.getTime());
// Check that the transaction is not older than 7 days.
if (transactionAge > 7) {
fields.set('e_company_survey_expired_flag_yn', CompanyYesNo.YES);
fields.set('e_company_survey_termination_flag_yn', CompanyYesNo.YES);
return true;
}
fields.set('e_company_survey_expired_flag_yn', CompanyYesNo.NO);
fields.set('e_company_store_st_code_survey_alt', C1);
switch (C1) {
case SurveyCode.CORE_STORE:
fields.set('e_company_store_st_flag_yn', CompanyYesNo.YES);
fields.set('e_company_store_cp_flag_yn', CompanyYesNo.NO);
break;
case SurveyCode.GENERAL:
fields.set('e_company_store_st_flag_yn', CompanyYesNo.NO);
fields.set('e_company_store_cp_flag_yn', CompanyYesNo.YES);
fields.set('e_company_store_cp_buy_type_alt', '1'); // TODO: Set reference variable for AltSet
fields.set('e_company_store_cp_buy_loc_alt', '1'); // TODO: Set reference variable for AltSet
break;
case SurveyCode.SHIPPING_PRODUCT:
fields.set('e_company_store_st_flag_yn', CompanyYesNo.NO);
fields.set('e_company_store_cp_flag_yn', CompanyYesNo.YES);
fields.set('e_company_store_cp_buy_type_alt', '2'); // TODO: Set reference variable for AltSet
fields.set('e_company_store_cp_buy_loc_alt', '1'); // TODO: Set reference variable for AltSet
break;
default:
// Unknown code
resetFields();
return false;
}
var transactionId = T1 + T2 + T3 + T4 + T5;
var registerCode = R1 + R2 + R3;
fields.set('e_company_transaction_id_txt', transactionId);
fields.set('e_company_store_st_register_code_txt', registerCode);
fields.set('e_company_store_receipt_survey_id_text', receiptCode);
fields.set('q_company_store_all_store_selector_unit', unit);
fields.set('e_company_store_unit', unit);
fields.set('e_company_transaction_datetime', year + '-' + month + '-' + day + 'T12:00:00-0500'); // TODO: Time zone fragility?
fields.set('e_company_global_survey_type_alt', '1'); // TODO: Set reference variable for AltSet
fields.set('e_company_survey_termination_flag_yn', CompanyYesNo.NO);
return true;
}());
/**
* Resets all fields back to an unset state.
*/
function resetFields() {
[
'q_company_store_all_receipt_code_alt',
'q_company_store_all_receipt_code_txt',
'e_company_survey_termination_flag_yn',
'q_company_store_all_store_selector_unit',
'e_company_survey_expired_flag_yn',
'e_company_store_st_code_survey_alt',
'e_company_store_st_flag_yn',
'e_company_store_cp_flag_yn',
'e_company_store_cp_buy_type_alt',
'e_company_store_cp_buy_loc_alt',
'e_company_transaction_id_txt',
'e_company_store_st_register_code_txt',
'e_company_store_receipt_survey_id_text',
'q_company_store_all_store_selector_unit',
'e_company_store_unit',
'e_company_transaction_datetime',
'e_company_global_survey_type_alt',
'e_company_survey_termination_flag_yn'
].forEach(function (fieldName, index, array) {
fields.set(fieldName, null);
});
}
/**
* Validates that the year, month, and day given as part of the receipt code
* translates to a valid Date object.
*
* @param date the Date object to check
* @param now the current day/time
* @param year the year value from the receipt code
* @param month the month value from the receipt code
* @param day the day value from the receipt code
*/
function isDateValid(date, now, year, month, day) {
return transactionDate.getFullYear() === year
&& transactionDate.getMonth() === month
&& transactionDate.getDate() === day
&& transactionDate.getTime() <= pacificToEasternTime(now).getTime();
}
/**
* Converts a time delta in milliseconds to number of days.
*
* @param msec the time delta in milliseconds
* @return the number of days, as an integer
*/
function msecToDays(msec) {
return Math.floor(msec / 86400000);
}
/**
* Converts a timestamp from Pacific Time to Eastern Time.
*/
function pacificToEasternTime(timestamp) {
return new Date(timestamp.getTime() + 3600000);
}
/**
* Validates the receipt code provided by the user. If the receipt code is valid, its components are stored into various fields for later reference.
*/
(function () {
// AltSet references
var CompanyYesNo = {
YES : 1,
NO : 2
};
// Read and sanitize the receipt code according to the following rules:
// - Remove all whitespace
// - Remove any asterisks (*) found at the beginning or end of the code
// - Remove any double quotes (") found at the beginning or end of the code
var receiptCode =
(fields.get('q_company_receipt_id_entry') || '')
.replace(/^[\*\u0022]*|[\*\u0022]*$|\s*/g, '');
var receiptCodeRegex = /^(\d{8})?R(\d{3})(\d{3})(\d{4})$/i;
var matches = receiptCode.match(receiptCodeRegex);
if (matches === null) {
// Not a valid receipt code
return false;
}
var transactionDate = matches[1];
var storeCode = matches[2];
var registerId = matches[3];
var transactionId = matches[4];
var storeId = '[STORE]' + storeCode;
var storeAltDb = fields.getAltById('q_company_store_selector', storeId);
if (storeAltDb === null) {
// Unknown store
return false;
}
var qualifyingRegisterRegex = /^4(10|0[1-9])$/i;
var isQualifyingRegister = registerId.match(qualifyingRegisterRegex) ? CompanyYesNo.YES : CompanyYesNo.NO;
fields.set('e_company_transaction_date_txt', transactionDate);
fields.set('q_company_store_selector', storeAltDb);
fields.set('e_company_qualifying_register_yn', isQualifyingRegister);
return true;
}());
Validate survey fields
Add a JavaScript validation on a survey field to have it check the answer's value and confirm that it matches certain requirements. This example validates that the field q_survey_code contains an 'R' followed by 10 digits. It then sets the field q_unit to the unit corresponding to the first two digits in the survey code.
var x = fields.get('q_survey_code');
var re = /^R(\d){10}$/;
if (x == null || !re.exec(x)) {
return false;
} else {
fields.set('q_unit', x.substring(1,3));
return true;
}
This example validates that the field q_unit_id contains a valid unit ID. If it does, the example sets the field q_unit to the corresponding unit.
var x = fields.get('q_unit_id');
var alt = fields.getAltById('q_unit', x);
if (alt == null) {
return false;
} else {
fields.set('q_unit', alt);
}
This example performs a receipt-code validation (usually used in anonymous surveys). The node is prompt to enter an access code (which must match one of the property identifiers stored for the company). The script converts the entry to upper case using .toUpperCase() . Next, .getAltById() validates the entry and .set() stores the identifier. The .set(), saves the response on Anonymous surveys.
var x = fields.get('q_surveycode').toUpperCase();
var re = /^.{2}-.{4}$/;
if (re.exec(x)) {
return false
} else {
var site = fields.getAltById('q_store', x);
if (site == null) {
return false;
} else {
fields.set('q_store', site);
return true;
}
}
