Amazon Connect contact record (previously called contact trace record or CTR) data is stored automatically in Voice Call records through data synchronization. Sometimes this sync doesn’t occur, and it isn’t always possible to access contact record data in Amazon Connect after the call. Back up contact record data to a separate Amazon S3 bucket, then check for Voice Call records that don’t have contact record data, and then resync the contact record data to your org using the backup data.
Ease of Implementation
Advanced
Estimated Time to Implement
2–3 hours
Contact Trace Records are generated on Amazon for each call. This data contains details about the call, including caller and rep information, call statistics, and queue information. The CTRDataSyncFunction Lambda function automatically runs for each call to store this data in a Voice Call record. CTRs aren’t retained by Amazon, and if the CTR sync process fails for any reason, you can lose important information about the call.
This solution provides a way to reattempt a sync of the CTR data by first backing up CTR data in a separate Amazon S3 bucket, then checking for any sync failures, and then re-uploading CTR data whenever necessary.
Prerequisites
To run this example:
Have your AWS root user or AWS administrator credentials ready.
Create an Amazon S3 bucket on the same instance as your contact center. This bucket must be accessible within the region where your Amazon instance is configured. When creating your S3 bucket, make sure that you set the right access control policies for the bucket so it’s not accessible outside of your instance.
Under General Configuration > Bucket Type, select General Purpose.
Under Object Ownership, select ACLs disabled (recommended).
Under Block Public Access settings for this bucket, select Block all public access.
Under Bucket Versioning, select Enable.
Under Default Encryption > Encryption Type, select Server-side encryption with Amazon S3 managed keys (SSE-S3).
Under Default Encryption > Bucket Key, select Enable.
When using the code provided by your S3 bucket service in AWS, replace the resource account ID and ARN with your account ID and S3 bucket ARN, respectively. Delete all code comments before you save your changes.
1{2 "Version": "2008-10-17",3 "Statement": [4{5 "Sid": "PutObjectAccess",6 "Effect": "Allow",7 "Principal":{8 "AWS": "arn:aws:iam::<your account id>:root" // REPLACE THIS WITH YOUR ACCOUNT ID9},10 "Action": "s3:*",11 "Resource": "<your S3 Bucket ARN>" // REPLACE THIS WITH YOUR S3 BUCKET ARN12},13{14 "Effect": "Allow",15 "Principal":{16 "Service": "transcribe.amazonaws.com"17},18 "Action": "s3:*",19 "Resource": "<your S3 Bucket ARN>" // REPLACE THIS WITH YOUR S3 BUCKET ARN20}21]22}
Step 2: Set Data Expiration
After you store the CTR data in the S3 bucket, it’s a good idea to purge them periodically. To remove old data, specify the object expiration for the S3 bucket and define how long to wait before the files are deleted. See Amazon’s documentation: Expiring Objects and Managing your storage lifecycle.
Step 3: Save Contract Trace Records in S3
The CTRDataSyncFunction is configured to receive events from the Kinesis stream. These events are generated whenever a CTR is generated. Modify this Lambda function to read those events and write them to the S3 bucket as they’re generated. Update the handler.js file so that it contains a new function, writeCTRToS3, that writes data to the new bucket. Then, update the existing code to call this new function.
When modifying existing code, it’s crucial to add code in the correct context. Inserting code snippets out of context or in the wrong order breaks the code.
In this example, the text in bold is where the new writeCTRToS3 function must be added to the handler.js file. Delete all code comments before you save your changes.
1const aws = require("aws-sdk");2const SCVLoggingUtil = require("./SCVLoggingUtil");3const s3 = new aws.S3();45const lambda = new aws.Lambda();6const utils = require("./utils");78function updateVoiceCallRecord(voiceCall){9 SCVLoggingUtil.info({10 message: "CTR/updateVoiceCallRecord Request created",11 context:{contactId: voiceCall.contactId},12});13 const payload = {14 Details:{15 Parameters:{16 methodName: "updateVoiceCall",17 fieldValues: voiceCall.fields,18 contactId: voiceCall.contactId,19},20},21};2223 const params = {24 FunctionName: process.env.INVOKE_TELEPHONY_INTEGRATION_API_ARN,25 Payload: JSON.stringify(payload),26};2728 return lambda.invoke(params).promise();29}3031function shouldProcessCtr(ctrRecord){32 return(33["INBOUND", "OUTBOUND", "TRANSFER", "CALLBACK", "API"].includes(34 ctrRecord.InitiationMethod35) &&36 ctrRecord.ContactId &&37 // if the call is a voicemail, no need to process it because the packager lambda updates the voicecall record38 !(39 ctrRecord.Attributes &&40 ctrRecord.Attributes.vm_flag &&41 ctrRecord.Recordings42)43);44}4546exports.handler = async(event)=>{47 const promises = [];48 SCVLoggingUtil.debug({49 message: "CTRDataSync event received",50 context: event,51});52 event.Records.forEach((record)=>{53 const ctr = utils.parseData(record.kinesis.data);54 if(shouldProcessCtr(ctr)){55 const voiceCall = utils.transformCTR(ctr);56 SCVLoggingUtil.debug({57 category: "ctrDataSync.handler",58 message: "Transformed CTR to voice call",59 context: voiceCall,60});61 const updatePromise = updateVoiceCallRecord(voiceCall);6263 promises.push(updatePromise);6465 updatePromise.then((response)=>{66 SCVLoggingUtil.info({67 message: "updateVoiceCallRecord response",68 context: response,69});70});7172 writeCTRToS3(ctr);7374}else{75 SCVLoggingUtil.error({76 message: "Encountered Non supported CTR Events: failing fast",77 context:{},78});79}80});8182 return Promise.all(promises);83};848586function writeCTRToS3(ctr){87 var params = {88 Bucket : "<your S3 bucket Name>", // REPLACE THIS WITH YOUR S3 BUCKET NAME89 Key: ctr.ContactId,90 Body: Buffer.from(JSON.stringify(ctr)),91}9293 s3.putObject(params, function(err, data){94 if(err){95 console.log("Error uploading CTR to S3: " + JSON.stringify(err));96}else{97 console.log("Successfully uploaded CTR to S3: " + JSON.stringify(data));98}99});100}101102103exports.shouldProcessCtr = shouldProcessCtr;
There’s one object in S3 for every CTR generated. The object is named with the ContactId of the CTR, so it’s easy to look up.
In the CTRDataSyncFunction associated with your contact center, create a new inline policy for the IAM role and delete any comments in the code before saving:
1{2 "Version": "2012-10-17",3 "Statement": [4{5 "Sid": "VisualEditor0",6 "Effect": "Allow",7 "Action":[8 "s3:PutObject",9 "s3:ListBucket",10 "s3:PutObjectAcl"11],12 "Resource": "<your S3 Bucket ARN>/*" // REPLACE THIS WITH YOUR S3 BUCKET ARN13}14]15}
Creating a new inline policy for the IAM role is required for the CTRDataSyncFunction to run, but it is not the only required policy. The other required policies come incorporated out of the box, but it’s a good idea to confirm that they’re present. These policies are:
Query for Voice Calls that aren’t synced. Query for all Voice Call records (over a given time period) that weren’t updated by the CTRDataSyncFunction. To identify Voice Calls that aren’t updated, you can filter by fields that are only updated during a CTR sync. In this example, we specify a condition based on CustomerHoldDuration.
1SELECT Id, VendorCallKey FROM VoiceCall WHERE CustomerHoldDuration = null
Fetch CTRs from S3. If there are results returned from the previous step, then read from the S3 bucket to fetch CTRs for the given Voice Call records. Look up this data with the VendorCallKey.
Update Voice Call records in the org. Update Voice Call records based on the information returned from the previous step.
This sample code assumes that there’s an environment variable named INVOKE_TELEPHONY_INTEGRATION_API_ARN (option a), but you can hard-code the ARN if you prefer.
Updates to sfRestApi.js. In-context changes are in bold. Remove any comments in the code before saving.
1const utils = require("./utils");2const axiosWrapper = require("./axiosWrapper");3const SCVLoggingUtil = require("./SCVLoggingUtil");45const aws = require('aws-sdk');6const lambda = new aws.Lambda();7const s3 = new aws.S3();89….10async function searchRecord(sosl){11 try{12 const response = await sendRequest(13 "get",14 `/search/?q=${encodeURIComponent(sosl)}`15);16 SCVLoggingUtil.debug({17 category: "sfRestApi.searchRecord",18 message: "search Record response",19 context: response,20});21 if(response.data.searchRecords.length === 0){22 return{};23}24 return response.data.searchRecords[0];25}catch(e){26 return buildError(e);27}28}2930// NEW function that performs CTR sync retry31async function retryCtrSync(soql){32 // Query For VoiceCalls that are missing CTR Sync33 const response = await sendRequest(34 "get",35 `/query/?q=${encodeURIComponent(soql)}`36);37 console.log("Querying for VoiceCalls that were not updated");3839 if(response.data.errorType){40 console.log("Error querying VoiceCalls: " + response.data.errorMessage);4142 return{43 success: false,44 errorMessage: response.data.errorMessage45};46}else if(response.data.totalSize === 0){47 console.log("No VoiceCalls were found that were missing CTR update");48 return{};49}else{50 console.log("Successfully queried for VoiceCalls");51 const result = response.data.records;5253 // Fetch CTRs from S354 var ctrRecords = await getCtrsFromS3(result);5556 // Update VoiceCalls with the CTRs fetched from S357 var results = await updateVoiceCallRecords(ctrRecords);58 return results;59}60}616263// NEW Function to fetch CTRs from S364async function getCtrsFromS3(result){65 var ctrData = [];66 for(var i=0; i<result.length; i++){67 console.log("Vendor Call Key " + result[i].VendorCallKey);68 var getParams = {69 Bucket: '<your S3 bucket Name>', // your bucket name,70 Key: result[i].VendorCallKey // path to the object you're looking for71}7273 await s3.getObject(getParams, function(err, data1){74 if(err){75 console.log("ERROR in querying S3 " + err);76 return err;77}else{78 var objectData = data1.Body.toString('utf-8'); // Use the encoding necessary79 var ctrObj = JSON.parse(objectData);80 console.log("CTR found for ContactId: " + ctrObj.ContactId);81 ctrData.push(ctrObj);82}83}).promise();84}85 return ctrData;86}8788// NEW function to update VoiceCall Records89async function updateVoiceCallRecords(ctrRecords){90 let promises = [];91 ctrRecords.forEach((record)=>{92 const ctr = record;9394 if(ctr.ContactId){95 const voiceCall = utils.transformCTR(ctr);96 promises.push(updateVoiceCallRecord(voiceCall));97}else{98 console.log("No ContactId found in CTR");99}100});101102 return await Promise.all(promises);103}104105// UPDATED: Add environment variable106function updateVoiceCallRecord(voiceCall){107 console.log("Updating VoiceCall");108 const payload = {109 Details:{110 Parameters:{111 methodName: 'updateVoiceCall',112 fieldValues: voiceCall.fields,113 contactId: voiceCall.contactId114}115}116};117118 const params = {119 // NEW: Create this as an environment variable 120 // or hard-code the ARN of the Lambda121 FunctionName: process.env.INVOKE_TELEPHONY_INTEGRATION_API_ARN,122123 Payload: JSON.stringify(payload)124};125 return lambda.invoke(params).promise();126}127128129// UPDATED: Include the new function (retryCtrSync)130module.exports = {131 createRecord,132 updateRecord,133 queryRecord,134 searchRecord,135 sendRealtimeAlertEvent,136 retryCtrSync,137};
Updates to QueryEngine.js. In-context changes are in bold. Remove any comments in the code before saving.
1const api = require("./sfRestApi");2const SCVLoggingUtil = require("./SCVLoggingUtil");34function formatQuery(args, queryStr){5 let query;6 Object.keys(args).forEach((key)=>{7 const replacement = `{${key}}`;8 query = queryStr.replace(replacement, args[key]);9});10 return query;11}1213// invokes the query from sf rest api14// can take the query as a formatted string of sorts,15// replacing {key} with its value in the js object16async function invokeQuery(query, args){17 const formattedQuery = formatQuery(args, query);18 SCVLoggingUtil.debug({19 message: "invoke query from SfRestApi",20 context:{payload: formattedQuery},21});22 return api.queryRecord(formattedQuery);23}242526// NEW function to that exposes the Retry CTR27async function retryCTRSync(query, args){28 var formattedQuery = formatQuery(args, query);29 return await api.retryCtrSync(formattedQuery);30}313233// UPDATED: Export the new function (retryCtrSync)34module.exports={35 invokeQuery,36 formatQuery,37 retryCTRSync38};
Updates to Handler.js. In-context changes are in bold:
1const flatten = require("flat");2const SCVLoggingUtil = require("./SCVLoggingUtil");3const api = require("./sfRestApi");4const queryEngine = require("./queryEngine");5const utils = require("./utils");6const SFSPhoneCallFlow = require("./SFSPhoneCallFlow");78// --------------- Events -----------------------9async function dispatchSearch(sosl){10 const searchResult = await api.searchRecord(sosl);11 return flatten(searchResult);12}131415async function dispatch_ctrsync(soql, event){16 const parameters = event.Details.Parameters;17 console.log(event)18 let response;19 try{20 const queryResult = await queryEngine.retryCTRSync(soql, parameters);21 response = {22 statusCode: 200,23 result: queryResult24}25}26 catch(e){27 response = {28 statusCode: e.response && e.response.status ? e.response.status : 500,29 result: e30}31}32 return flatten(response);33}3435// --------------- Main handler -----------------------;36 case "SFSPhoneCallFlowQuery": {37 const res = await SFSPhoneCallFlow.entryPoint(event);38 result = flatten(res);39 break;40}4142 case 'retryCTRSync': {43 result = dispatch_ctrsync(soql, event);44 break;45}464748 default: {49 SCVLoggingUtil.warn({50 message: "Unsupported method",51 context: { payload: event},52});53 throw new Error(`Unsupported method: ${methodName}`);54}55}5657 if(result.success === false){58 throw new Error(result.errorMessage);59}else{60 return result;61}62};6364}
Updates to utils.js. In-context changes are in bold. Remove any comments in the code before saving.
The transformCTR and getCallAttributes functions are copied over from the CTRDataSyncFunctionutils.js file. (Alternatively, directly modify CTRDataSyncFunction to support taking a single CTR and updating the Voice Call record from there.) These functions must be inserted under the declaration of the getRealtimeAlertEventFieldValuesFromConnectLambdaParams function.
1function getRealtimeAlertEventFieldValuesFromConnectLambdaParams(params){2 const fieldValues = {};3 Object.entries(params).forEach((entry)=>{4 const key = entry[0];5 if(key !== "methodName"){6 fieldValues[key] = entry[1];7}8});9 return fieldValues;10}1112// COPIED function to transform CTR 13function transformCTR(ctr){14 const voiceCall = {};1516 voiceCall.startTime = ctr.InitiationTimestamp;17 voiceCall.endTime = ctr.DisconnectTimestamp;18 voiceCall.parentCallIdentifier = ctr.PreviousContactId;1920 if(ctr.Agent){21 voiceCall.acceptTime = ctr.Agent.ConnectedToAgentTimestamp;22 voiceCall.totalHoldDuration = ctr.Agent.CustomerHoldDuration;23 voiceCall.longestHoldDuration = ctr.Agent.LongestHoldDuration;24 voiceCall.agentInteractionDuration = ctr.Agent.AgentInteractionDuration;25 voiceCall.numberOfHolds = ctr.Agent.NumberOfHolds;26}2728 if(ctr.Queue){29 voiceCall.enqueueTime = ctr.Queue.EnqueueTimestamp;30 voiceCall.queue = ctr.Queue.Name;31}3233 if(ctr.InitiationMethod){34 voiceCall.initiationMethod = ctr.InitiationMethod;35 if(ctr.InitiationMethod === 'OUTBOUND'){36 if(ctr.SystemEndpoint){37 voiceCall.fromNumber = ctr.SystemEndpoint.Address;38}39 if(ctr.CustomerEndpoint){40 voiceCall.toNumber = ctr.CustomerEndpoint.Address;41}42}else{43 if(ctr.SystemEndpoint){44 voiceCall.toNumber = ctr.SystemEndpoint.Address;45}46 if(ctr.CustomerEndpoint){47 voiceCall.fromNumber = ctr.CustomerEndpoint.Address;48}49}50}5152 if(ctr.Recording){53 voiceCall.recordingLocation = ctr.Recording.Location;54}5556 // Check if there are custom contact attributes 57 if(ctr.Attributes){58 var callAttributes = {};5960 // Get contact attributes data into call attributes61 callAttributes = getCallAttributes(ctr.Attributes);6263 voiceCall.callAttributes = callAttributes;64}6566 Object.keys(voiceCall).forEach(function(key){67 if(voiceCall[key] === null || voiceCall[key] === undefined){68 delete voiceCall[key];69}70});7172 return{contactId: ctr.ContactId, fields: voiceCall};73}7475// COPIED function to filter call attributes76/**77 * Filter call attributes to be included in API payload based on prefix and strip prefix78 * 79 * @param{object} rawCallAttributes - Contact flow attributes80 * 81 * @return{string} - Stringified contact flow attributes with prefix removed82 */83function getCallAttributes(rawCallAttributes){84 const prefix = 'sfdc-';85 const prefixLen = prefix.length;86 let callAttributes = {};8788 for(const key in rawCallAttributes){89 if(rawCallAttributes.hasOwnProperty(key) && key.startsWith(prefix)){90 callAttributes[key.substring(prefixLen)] = rawCallAttributes[key];91}92 // Set SCV Limits Error if the specific contact attribute is set93 if(rawCallAttributes.sf_realtime_transcription_status){94 callAttributes.sf_realtime_transcription_status = rawCallAttributes.sf_realtime_transcription_status;95}96}9798 return JSON.stringify(callAttributes);99}100101102// UPDATED: Export the two new functions (transformCTR, getCallAttributes)103module.exports = {104 generateJWT,105 getAccessToken,106 formatObjectApiName,107 getSObjectFieldValuesFromConnectLambdaParams,108109 transformCTR,110 getCallAttributes111112};
Adding the AmazonS3FullAccess policy to the role is required for the CTRDataSyncFunction to run, but it’s not the only required policy. The other required policies come incorporated out of the box, but it’s a good idea to confirm that they’re present. These policies are:
Depending on how your org is configured, out of the box policies can come with additional attributes.
Important
Next, publish a new version of the Lambda functions. Make sure that you update this code in your Lambda functions to ensure compatibility after the contact center update.
Step 6: Call the New Method on the InvokeSalesforceRestApiFunction Lambda
Dial your phone number in order to create a Voice Call record. After the call is answered, hang up.
In your org, find the newly created Voice Call record and remove some CTR fields, including CustomerHoldDuration.
Call InvokeSalesforceRestApiFunction with the information specified in the final step (step 6) of this example. However, instead of scheduling the function to run, you can programmatically call the Lambda from the console as shown in Amazon’s documentation (Create a Lambda function with the console). Use a payload similar to this:
1{2 "Details": {3 "Parameters":{4 "methodName": "retryCTRSync",5 "soql": "SELECT Id, VendorCallKey FROM VoiceCall WHERE CustomerHoldDuration = null"6}7}8}
Check that the Voice Call record now has the CTR fields filled in.