2024-04-04 16:13:53 +00:00
module . exports = {
friendlyName : 'Save questionnaire progress and continue' ,
description : 'Saves the user\'s current progress in the get started questionnaire' ,
inputs : {
currentStep : {
type : 'string' ,
description : 'The step of the get started questionnaire that is being saved.' ,
isIn : [
'start' ,
'what-are-you-using-fleet-for' ,
'have-you-ever-used-fleet' ,
'how-many-hosts' ,
'will-you-be-self-hosting' ,
'what-are-you-working-on-eo-security' ,
2024-04-08 10:39:19 +00:00
'what-does-your-team-manage-eo-it' ,
'what-does-your-team-manage-vm' ,
'what-do-you-manage-mdm' ,
2024-10-03 18:49:42 +00:00
'message-about-cross-platform-mdm' ,
2024-04-04 16:13:53 +00:00
'is-it-any-good' ,
'what-did-you-think' ,
'deploy-fleet-in-your-environment' ,
'managed-cloud-for-growing-deployments' ,
'self-hosted-deploy' ,
2024-05-01 23:22:47 +00:00
'whats-left-to-get-you-set-up' ,
'how-was-your-deployment' ,
2024-06-05 01:52:25 +00:00
'thanks-for-checking-out-fleet' ,
2024-04-04 16:13:53 +00:00
]
} ,
formData : {
type : { } ,
description : 'The formdata that will be saved for this step of the get started questionnaire'
2024-04-08 10:39:19 +00:00
} ,
2024-04-04 16:13:53 +00:00
} ,
exits : {
2024-04-08 10:39:19 +00:00
success : {
outputDescription : 'All get started questionnaire answers accumulated so far by this user.' ,
outputType : { }
} ,
2024-04-04 16:13:53 +00:00
} ,
fn : async function ( { currentStep , formData } ) {
// find this user's DB record.
2024-04-08 10:39:19 +00:00
let userRecord = this . req . me ;
2024-04-04 16:13:53 +00:00
let questionnaireProgress ;
// If this user doesn't have a lastSubmittedGetStartedQuestionnaireStep or getStartedQuestionnaireAnswers, create an empty dictionary to store their answers.
if ( ! userRecord . lastSubmittedGetStartedQuestionnaireStep || _ . isEmpty ( userRecord . getStartedQuestionnaireAnswers ) ) {
questionnaireProgress = { } ;
} else { // other wise clone it from the user record.
questionnaireProgress = _ . clone ( userRecord . getStartedQuestionnaireAnswers ) ;
}
2024-04-20 23:27:17 +00:00
// Tease out what liur buying situation will now be (or is and was, if it's not changing)
2024-04-21 00:02:06 +00:00
let primaryBuyingSituation = formData . primaryBuyingSituation === undefined ? this . req . me . primaryBuyingSituation : formData . primaryBuyingSituation ;
2024-04-20 23:27:17 +00:00
2024-04-04 16:13:53 +00:00
// When the 'what-are-you-using-fleet-for' is completed, update this user's DB record and session to include their answer.
if ( currentStep === 'what-are-you-using-fleet-for' ) {
2024-04-19 02:18:36 +00:00
await User . updateOne ( { id : this . req . me . id } )
. set ( {
2024-04-20 23:27:17 +00:00
primaryBuyingSituation : primaryBuyingSituation
2024-04-08 10:39:19 +00:00
} ) ;
2024-04-04 16:13:53 +00:00
// Set the primary buying situation in the user's session.
this . req . session . primaryBuyingSituation = primaryBuyingSituation ;
2024-04-20 23:27:17 +00:00
} //fi
2024-04-19 02:18:36 +00:00
// ┌─┐┌─┐┌┬┐ ┌─┐┌─┐┬ ┬┌─┐┬ ┬┌─┐┬ ┌─┐┌─┐┬┌─┐┌─┐┬ ┌─┐┌┬┐┌─┐┌─┐┌─┐
// └─┐├┤ │ ├─┘└─┐└┬┘│ ├─┤│ ││ │ ││ ┬││ ├─┤│ └─┐ │ ├─┤│ ┬├┤
// └─┘└─┘ ┴ ┴ └─┘ ┴ └─┘┴ ┴└─┘┴─┘└─┘└─┘┴└─┘┴ ┴┴─┘ └─┘ ┴ ┴ ┴└─┘└─┘
// This is how the questionnaire steps/options change a user's psychologicalStage value.
// 'start': No change
2024-04-20 23:27:17 +00:00
// 'what-are-you-using-fleet-for':
// - (any option) = stage 2
2024-04-19 02:18:36 +00:00
// 'have-you-ever-used-fleet':
// - yes-deployed: » Stage 6
2024-05-02 16:25:48 +00:00
// - yes-recently-deployed: » Stage 5
2024-04-20 23:27:17 +00:00
// - yes-deployed-local: » Stage 3 (Tried Fleet but might not have a use case)
// - yes-deployed-long-time: Stage 2 (Tried Fleet long ago but might not fully grasp)
// - no: Stage 2 (Never tried Fleet and might not fully grasp)
2024-05-02 16:25:48 +00:00
// 'how-many-hosts': Stage 4/5/6
// 'will-you-be-self-hosting': Stage 5/6
2024-04-19 02:18:36 +00:00
// 'what-are-you-working-on-eo-security'
// - All other options » Stage 4
// 'what-does-your-team-manage-eo-it'
// - All other options » Stage 4
// 'what-does-your-team-manage-vm'
// - All other options » Stage 4
// 'what-do-you-manage-mdm'
2024-08-21 17:54:06 +00:00
// - no-use-case-yet: » Stage 3
2024-04-19 02:18:36 +00:00
// - All other options » Stage 4
2024-08-21 17:54:06 +00:00
// 'is-it-any-good': Stage 3/4 (depends on answer from 'have-you-ever-used-fleet' & the buying situation specific step)
2024-04-19 02:18:36 +00:00
// 'what-did-you-think'
2024-08-21 17:54:06 +00:00
// - host-fleet-for-me » Stage 5
// - deploy-fleet-in-environment » Stage 5
2024-04-26 17:42:48 +00:00
// - let-me-think-about-it » Stage 2
2024-05-02 16:25:48 +00:00
// FUTURE: Should the step about deploying fleet in your env be here? (For same reason is-it-any-good is here: when navigating back then forwards?)
2024-05-01 23:22:47 +00:00
// 'how-was-your-deployment'
2024-05-02 16:25:48 +00:00
// - up-and-running » Stage 5
2024-08-21 17:54:06 +00:00
// - kinda-stuck » Stage 5
// - havent-gotten-to-it » Stage 5
// - changed-mind-want-managed-deployment » Stage 5
2024-05-01 23:22:47 +00:00
// - decided-to-not-use-fleet » Stage 2
// 'whats-left-to-get-you-set-up'
2024-05-02 16:25:48 +00:00
// - need-premium-license-key » No change (Stage ??)
// - help-show-fleet-to-my-team » No change (Stage ??)
// - procurement-wants-some-stuff » No change (Stage ??)
// - nothing » No change (Stage ??)
2024-05-01 23:22:47 +00:00
2024-04-19 02:18:36 +00:00
let psychologicalStage = userRecord . psychologicalStage ;
2024-07-27 01:25:26 +00:00
let psychologicalStageLastChangedAt = userRecord . psychologicalStageLastChangedAt ;
2024-04-19 02:18:36 +00:00
// Get the value of the submitted formData, we do this so we only need to check one variable, instead of (formData.attribute === 'foo');
let valueFromFormData = _ . values ( formData ) [ 0 ] ;
2024-04-26 17:42:48 +00:00
if ( currentStep === 'start' ) {
// There is change when the user completes the start step.
} else if ( currentStep === 'what-are-you-using-fleet-for' ) {
2024-04-20 23:27:17 +00:00
psychologicalStage = '2 - Aware' ;
} else if ( currentStep === 'have-you-ever-used-fleet' ) {
2024-08-21 17:54:06 +00:00
if ( valueFromFormData === 'yes-deployed' ) {
2024-04-19 02:18:36 +00:00
// If the user has Fleet deployed, set their stage to 6.
psychologicalStage = '6 - Has team buy-in' ;
2024-08-21 17:54:06 +00:00
} else if ( valueFromFormData === 'yes-recently-deployed' ) {
2024-05-02 16:25:48 +00:00
psychologicalStage = '5 - Personally confident' ;
2024-04-20 23:27:17 +00:00
} else {
2024-08-21 17:54:06 +00:00
psychologicalStage = '3 - Intrigued' ;
2024-04-19 02:18:36 +00:00
}
2024-04-26 17:42:48 +00:00
} else {
// If the user submitted any other step, we'll set variables using the answers to the previous questions.
// Get the user's selected primaryBuyingSiutation.
let currentSelectedBuyingSituation = questionnaireProgress [ 'what-are-you-using-fleet-for' ] . primaryBuyingSituation ;
// Get the user's answer to the "Have you ever used Fleet?" question.
let hasUsedFleetAnswer = questionnaireProgress [ 'have-you-ever-used-fleet' ] . fleetUseStatus ;
if ( [ 'what-are-you-working-on-eo-security' , 'what-does-your-team-manage-eo-it' , 'what-does-your-team-manage-vm' , 'what-do-you-manage-mdm' ] . includes ( currentStep ) ) {
if ( valueFromFormData === 'no-use-case-yet' ) {
2024-08-21 17:54:06 +00:00
psychologicalStage = '3 - Intrigued' ;
2024-04-26 17:42:48 +00:00
} else { // Otherwise, they have a use case and will be set to stage 4.
psychologicalStage = '4 - Has use case' ;
}
2024-09-12 22:37:48 +00:00
// When the user submits the step before the "Is it any good?" step, we will generate them a 30 day Trial key for Fleet Premium that they can use with fleetctl preview
if ( ! userRecord . fleetPremiumTrialLicenseKey ) {
let thirtyDaysFromNowAt = Date . now ( ) + ( 1000 * 60 * 60 * 24 * 30 ) ;
let trialLicenseKeyForThisUser = await sails . helpers . createLicenseKey . with ( {
numberOfHosts : 10 ,
2024-09-17 23:00:58 +00:00
organization : this . req . me . organization ? this . req . me . organization : 'Fleet Premium trial' ,
2024-09-12 22:37:48 +00:00
expiresAt : thirtyDaysFromNowAt ,
} ) ;
// Save the trial license key to the DB record for this user.
await User . updateOne ( { id : this . req . me . id } )
. set ( {
fleetPremiumTrialLicenseKey : trialLicenseKeyForThisUser ,
fleetPremiumTrialLicenseKeyExpiresAt : thirtyDaysFromNowAt ,
} ) ;
}
2024-04-26 17:42:48 +00:00
} else if ( currentStep === 'is-it-any-good' ) {
if ( currentSelectedBuyingSituation === 'mdm' ) {
2024-05-02 16:25:48 +00:00
// Since the mdm use case question is the only buying situation-specific question where a use case can't
// be selected, we'll check the user's previous answers before changing their psyStage
2024-04-26 17:42:48 +00:00
if ( questionnaireProgress [ 'what-do-you-manage-mdm' ] . mdmUseCase === 'no-use-case-yet' ) {
// Check the user's answer to the have-you-ever-used-fleet question.
2024-08-21 17:54:06 +00:00
psychologicalStage = '3 - Intrigued' ;
2024-04-26 17:42:48 +00:00
} else {
psychologicalStage = '4 - Has use case' ;
}
} else { // For any other selected primary buying situation, since a use case will have been selected, set their psyStage to 4
psychologicalStage = '4 - Has use case' ;
// FUTURE: check previous answers for other selected buying situations.
}
2024-05-02 16:25:48 +00:00
} else if ( currentStep === 'what-did-you-think' ) { // (what did you think about [presumably after you actually did...] trying it locally)
2024-05-01 23:22:47 +00:00
// If the user selects "Let me think about it", set their psyStage to 2.
if ( valueFromFormData === 'let-me-think-about-it' ) {
2024-04-26 17:42:48 +00:00
psychologicalStage = '2 - Aware' ;
2024-08-21 17:54:06 +00:00
} else if ( [ 'host-fleet-for-me' , 'deploy-fleet-in-environment' ] . includes ( valueFromFormData ) ) {
psychologicalStage = '5 - Personally confident' ;
2024-05-02 16:25:48 +00:00
} else { require ( 'assert' ) ( false , 'This should never happen.' ) ; }
2024-05-01 23:22:47 +00:00
} else if ( currentStep === 'how-was-your-deployment' ) {
if ( valueFromFormData === 'decided-to-not-use-fleet' ) {
psychologicalStage = '2 - Aware' ;
2024-08-21 17:54:06 +00:00
} else if ( [ 'up-and-running' , 'changed-mind-want-managed-deployment' , 'kinda-stuck' , 'havent-gotten-to-it' ] . includes ( valueFromFormData ) ) {
2024-05-02 16:25:48 +00:00
psychologicalStage = '5 - Personally confident' ;
} else { require ( 'assert' ) ( false , 'This should never happen.' ) ; }
} else if ( currentStep === 'whats-left-to-get-you-set-up' ) {
// FUTURE: do more stuff (for now this always acts like 'no change')
2024-04-26 17:42:48 +00:00
} else if ( currentStep === 'how-many-hosts' ) {
2024-05-02 16:25:48 +00:00
if ( [ 'yes-deployed' ] . includes ( hasUsedFleetAnswer ) ) {
2024-05-02 00:01:56 +00:00
psychologicalStage = '6 - Has team buy-in' ;
2024-05-02 16:25:48 +00:00
} else {
2024-08-21 17:54:06 +00:00
psychologicalStage = '5 - Personally confident' ;
2024-05-02 00:01:56 +00:00
}
2024-04-26 17:42:48 +00:00
} else if ( currentStep === 'will-you-be-self-hosting' ) {
2024-05-02 16:25:48 +00:00
if ( [ 'yes-deployed' ] . includes ( hasUsedFleetAnswer ) ) {
2024-05-02 00:01:56 +00:00
psychologicalStage = '6 - Has team buy-in' ;
2024-05-07 21:46:00 +00:00
} else if ( [ 'yes-recently-deployed' ] . includes ( hasUsedFleetAnswer ) ) {
2024-05-02 00:01:56 +00:00
psychologicalStage = '5 - Personally confident' ;
2024-05-02 16:25:48 +00:00
} else { require ( 'assert' ) ( false , 'This should never happen.' ) ; }
2024-06-05 01:52:25 +00:00
} else if ( currentStep === 'thanks-for-checking-out-fleet' ) {
psychologicalStage = '2 - Aware' ;
2024-04-26 17:42:48 +00:00
} //fi
2024-04-19 02:18:36 +00:00
} //fi
2024-08-22 18:34:13 +00:00
// Set the user's answer to the current step.
questionnaireProgress [ currentStep ] = formData ;
// Clone the questionnaireProgress to prevent any mutations from sending it through the updateOne Waterline method.
let getStartedProgress = _ . clone ( questionnaireProgress ) ;
let questionnaireProgressAsAFormattedString = undefined ; // Default to undefined.
// Using a try catch block to handle errors from JSON.stringify.
try {
questionnaireProgressAsAFormattedString = JSON . stringify ( getStartedProgress )
. replace ( /[\{|\}|"]/g , '' ) // Remove the curly braces and quotation marks wrapping JSON objects
. replace ( /,/g , '\n' ) // Replace commas with newlines.
2024-10-03 18:49:42 +00:00
. replace ( /:\w+:/g , ':\t' ) // Replace the key from the formData with a colon and tab, (e.g., what-are-you-using-fleet-for:primaryBuyingSituation:eo-security, » what-are-you-using-fleet-for: eo-security)
. replace ( /(true)/g , 'step completed' ) ; // Replace any "true" answers with "step completed".
2024-08-22 18:34:13 +00:00
} catch ( err ) {
sails . log . warn ( ` When converting a user's (email: ${ this . req . me . emailAddress } ) getStartedQuestionnaireAnswers to a formatted string to send to the CRM, and error occurred ` , err ) ;
}
2024-10-24 21:41:43 +00:00
// Prepend the user's reported organization to the questionnaireProgressAsAFormattedString
2024-11-01 21:11:00 +00:00
questionnaireProgressAsAFormattedString = ` organization-according-to-fleetdm.com: ${ this . req . me . organization } \n ` + questionnaireProgressAsAFormattedString ;
2024-10-24 21:41:43 +00:00
2024-10-03 18:49:42 +00:00
// Create a dictionary of values to send to the CRM for this user.
let contactInformation = {
emailAddress : this . req . me . emailAddress ,
firstName : this . req . me . firstName ,
lastName : this . req . me . lastName ,
primaryBuyingSituation : primaryBuyingSituation === 'eo-security' ? 'Endpoint operations - Security' : primaryBuyingSituation === 'eo-it' ? 'Endpoint operations - IT' : primaryBuyingSituation === 'mdm' ? 'Device management (MDM)' : primaryBuyingSituation === 'vm' ? 'Vulnerability management' : undefined ,
organization : this . req . me . organization ,
psychologicalStage ,
getStartedResponses : questionnaireProgressAsAFormattedString ,
contactSource : 'Website - Sign up' ,
} ;
// If the user's psychologicalStage changes, add a psychologicalStageChangeReason to the contactInformation dictionary that we'll update the CRM record with.
2024-05-03 22:23:05 +00:00
if ( psychologicalStage !== userRecord . psychologicalStage ) {
2024-08-24 19:40:12 +00:00
let psychologicalStageChangeReason = 'Website - Organic start flow' ; // Default psystageChangeReason to "Website - Organic start flow"
if ( this . req . session . adAttributionString && this . req . session . visitedSiteFromAdAt ) {
2024-10-30 14:53:03 +00:00
let sevenDaysAgoAt = Date . now ( ) - ( 1000 * 60 * 60 * 24 * 7 ) ;
2024-08-24 19:40:12 +00:00
// If this user visited the website from an ad, set the psychologicalStageChangeReason to be the adCampaignId stored in their session.
2024-10-30 14:53:03 +00:00
if ( this . req . session . visitedSiteFromAdAt > sevenDaysAgoAt ) {
2024-08-24 19:40:12 +00:00
psychologicalStageChangeReason = this . req . session . adAttributionString ;
}
}
2024-10-03 18:49:42 +00:00
contactInformation . psychologicalStageChangeReason = psychologicalStageChangeReason ;
// Update the psychologicalStageLastChangedAt timestamp if the user's psychological stage has changed (otherwise this is set to the current value)
2024-07-27 01:25:26 +00:00
psychologicalStageLastChangedAt = Date . now ( ) ;
2024-05-03 22:29:56 +00:00
} //fi
2024-10-03 18:49:42 +00:00
// Update the CRM record for this user.
sails . helpers . salesforce . updateOrCreateContactAndAccount . with ( contactInformation ) . exec ( ( err ) => {
2025-01-29 00:47:32 +00:00
// Check to see if the error returned is related to duplicate records.
if ( err && err . errorCode === 'DUPLICATES_DETECTED' ) {
// Because we create/update CRM records in the background, it is possible to complete the first steps of the get started questionnaire before any CRM records are created.
// If the CRM helper returns an error related to a duplicate record, we will log a message if it occured when a user submitted one of the first three steps of the questionnaire.
if ( [ 'start' , 'what-are-you-using-fleet-for' , 'have-you-ever-used-fleet' ] . includes ( currentStep ) ) {
sails . log . verbose ( ` Background task failed: When a user (email: ${ this . req . me . emailAddress } submitted a step of the get started questionnaire ( ${ currentStep } ), a Contact and Account record could not be created/updated in the CRM because a duplicate record was found. ` , err ) ;
} else {
// If this was not one of the first three steps, log a warning to alert us.
sails . log . warn ( ` Background task failed: When a user (email: ${ this . req . me . emailAddress } submitted a step of the get started questionnaire ( ${ currentStep } ), a Contact and Account record could not be created/updated in the CRM because a duplicate record was found. ` , err ) ;
}
} else if ( err ) {
// If it is any other kind of error or t, log a warning.
sails . log . warn ( ` Background task failed: When a user (email: ${ this . req . me . emailAddress } submitted a step of the get started questionnaire ( ${ currentStep } ), a Contact and Account record could not be created/updated in the CRM. ` , err ) ;
2024-10-03 18:49:42 +00:00
}
return ;
} ) ;
2024-04-04 16:13:53 +00:00
// Update the user's database model.
2024-04-19 02:18:36 +00:00
await User . updateOne ( { id : userRecord . id } )
. set ( {
2024-04-08 10:39:19 +00:00
getStartedQuestionnaireAnswers : questionnaireProgress ,
2024-04-19 02:18:36 +00:00
lastSubmittedGetStartedQuestionnaireStep : currentStep ,
2024-07-27 01:25:26 +00:00
psychologicalStage ,
psychologicalStageLastChangedAt ,
2024-04-08 10:39:19 +00:00
} ) ;
2024-04-04 16:13:53 +00:00
// Return the JSON dictionary of form data submitted by this user.
2024-08-22 03:58:15 +00:00
return { getStartedProgress , psychologicalStage , primaryBuyingSituation } ;
2024-04-04 16:13:53 +00:00
}
} ;