Newer Version Available
Retry CTR Sync for VoiceCall Records
| Ease of Implementation | Advanced |
| Estimated Time to Implement | 2–3 hours |
Contact Trace Records are generated on Amazon for each call. This data contains the details about the call including caller and agent information, call statistics, and queue information. The CTRDataSyncFunction Lambda automatically runs for each call in order to store this data in a VoiceCall record. CTRs aren’t retained by Amazon and if the CTR sync process fails for any reason, you could 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
- Have your AWS root user or AWS administrator credentials ready.
- Be familiar with Amazon S3 buckets. To learn more, see Amazon’s documentation: Creating and configuring an S3 bucket.
- Be familiar with modifying Amazon Connect Lambda functions. To learn more, see Amazon’s documentation: AWS Lambda Developer Guide.
Step 1: Create an S3 Bucket
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. To learn more, see Amazon’s documentation: Expiring Objects and Managing your storage lifecycle.
Step 3: Save Contract Trace Records in S3
The CTRDataSyncFunction Lambda 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.
Changes to handler.js:
1// Include the aws-sdk module
2const aws = require('aws-sdk');
3const s3 = new aws.S3();
4
5// NEW function to write to S3
6function writeCTRToS3(ctr) {
7 var params = {
8 Bucket : "ctrsyncbucket",
9 Key: ctr.ContactId,
10 Body: Buffer.from(JSON.stringify(ctr)),
11 }
12
13 s3.putObject(params, function(err, data) {
14 if (err) {
15 console.log("Error uploading CTR to S3: " + JSON.stringify(err));
16 } else {
17 console.log("Successfully uploaded CTR to S3: " + JSON.stringify(data));
18 }
19 });
20}
21
22// UPDATED: Modify existing Lambda function handler to include call to write to S3
23exports.handler = async (event) => {
24 let promises = [];
25
26 event.Records.forEach((record) => {
27 const payload = Buffer.from(record.kinesis.data, 'base64').toString('ascii');
28 const ctr = JSON.parse(payload);
29 if (ctr.ContactId) {
30 writeCTRToS3(ctr); // ADD THIS LINE: Call to write to S3
31 const voiceCall = utils.transformCTR(ctr);
32 promises.push(updateVoiceCallRecord(voiceCall));
33 } else {
34 console.log("No ContactId found in CTR: " + JSON.stringify(ctr));
35 }
36 });
37
38 return await Promise.all(promises);
39};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.
Step 4: Re-Sync VoiceCalls That Aren’t Synced
Now, modify the InvokeSalesforceRestApiFunction Lambda to perform three key actions:
-
Query for VoiceCalls that aren’t synced. Query for all VoiceCall records (over
a given time period) that weren’t updated by CTRDataSyncFunction. To identify
VoiceCalls 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 VoiceCall records. Look up this data with the VendorCallKey.
- Update VoiceCall records in the org. Update VoiceCall records based on the information returned from the previous step.
The following code updates to InvokeSalesforceRestApiFunction perform all of the above actions.
Updates to sfRestApi.js.
1// Include required JS Modules
2const aws = require('aws-sdk');
3const lambda = new aws.Lambda();
4const s3 = new aws.S3();
5
6// NEW function that performs CTR sync retry
7async function retryCtrSync(soql) {
8 // Query For VoiceCalls that are missing CTR Sync
9 const accessToken = await utils.getAccessToken();
10 const response = await axios.get(`/query/?q=${encodeURIComponent(soql)}`, {
11 headers: {
12 'Authorization': `Bearer ${accessToken}`
13 }
14 });
15 console.log("Querying for VoiceCalls that were not updated");
16
17 if (response.data.errorType) {
18 console.log("Error querying VoiceCalls: " + response.data.errorMessage);
19
20 return {
21 success: false,
22 errorMessage: response.data.errorMessage
23 };
24 } else if (response.data.totalSize === 0) {
25 console.log("No VoiceCalls were found that were missing CTR update");
26 return {};
27 } else {
28 console.log("Successfully queried for VoiceCalls");
29 const result = response.data.records;
30
31 // Fetch CTRs from S3
32 var ctrRecords = await getCtrsFromS3(result);
33
34 // Update VoiceCalls with the CTRs fetched from S3
35 var results = await updateVoiceCallRecords(ctrRecords);
36 return results;
37 }
38}
39
40// NEW Function to fetch CTRs from S3
41async function getCtrsFromS3(result) {
42 var ctrData = [];
43 for (i=0;i<result.length;i++) {
44 console.log("Vendor Call Key " + result[i].VendorCallKey);
45 var getParams = {
46 Bucket: 'ctrsyncbucket', // your bucket name,
47 Key: result[i].VendorCallKey // path to the object you're looking for
48 }
49
50 await s3.getObject(getParams, function(err, data1) {
51 if (err) {
52 console.log("ERROR in querying S3 " + err);
53 return err;
54 } else {
55 var objectData = data1.Body.toString('utf-8'); // Use the encoding necessary
56 var ctrObj = JSON.parse(objectData);
57 console.log("CTR found for ContactId: " + ctrObj.ContactId);
58 ctrData.push(ctrObj);
59 }
60 }).promise();
61 }
62 return ctrData;
63}
64
65// NEW function to update VoiceCall Records
66async function updateVoiceCallRecords(ctrRecords) {
67 let promises = [];
68 ctrRecords.forEach((record) => {
69 const ctr = record;
70
71 if (ctr.ContactId) {
72 const voiceCall = utils.transformCTR(ctr);
73 promises.push(updateVoiceCallRecord(voiceCall));
74 } else {
75 console.log("No ContactId found in CTR");
76 }
77 });
78
79 return await Promise.all(promises);
80}
81
82// UPDATED: Add environment variable
83function updateVoiceCallRecord(voiceCall) {
84 console.log("Updating VoiceCall");
85 const payload = {
86 Details: {
87 Parameters: {
88 methodName: 'updateVoiceCall',
89 fieldValues: voiceCall.fields,
90 contactId: voiceCall.contactId
91 }
92 }
93 };
94
95 const params = {
96 // NEW: Create this as an environment variable
97 // or hard-code the ARN of the Lambda
98 FunctionName: process.env.INVOKE_TELEPHONY_INTEGRATION_API_ARN,
99 Payload: JSON.stringify(payload)
100 };
101 return lambda.invoke(params).promise();
102}
103
104// UPDATED: Include the new function (retryCtrSync)
105module.exports = {
106 createRecord,
107 updateRecord,
108 queryRecord,
109 retryCtrSync,
110 searchRecord,
111};Updates to QueryEngine.js.
1// NEW function to that exposes the Retry CTR
2async function retryCTRSync(query, args) {
3 formattedQuery = formatQuery(args, query);
4 return await api.retryCtrSync(formattedQuery);
5}
6
7// UPDATED: Export the new function (retryCtrSync)
8module.exports={
9 invokeQuery,
10 formatQuery,
11 retryCTRSync
12};Updates to Handler.js.
1// NEW function to dispatch CTR sync
2async function dispatch_ctrsync(soql, event){
3 const parameters = event.Details.Parameters;
4 let response;
5 try {
6 const queryResult = await queryEngine.retryCTRSync(soql, parameters);
7 response = {
8 statusCode: 200,
9 result: queryResult
10 }
11 }
12 catch (e) {
13 response = {
14 statusCode: e.response.status ? e.response.status : 500,
15 result: e
16 }
17 }
18 return flatten(response);
19}
20
21// UPDATED: Add a new case statement to the handler
22case 'retryCTRSync':
23 result = dispatch_ctrsync(soql, event);
24 break;Updates to utils.js.
The following two functions (transformCTR and getCallAttributes) are copied over from CTRDataSyncFunction Lambda utils.js. (Alternatively, directly modify CTRDataSyncFunction Lambda to support taking a single CTR and updating the VoiceCall record from there.)
1// COPIED function to transform CTR
2function transformCTR(ctr) {
3 const voiceCall = {};
4
5 voiceCall.startTime = ctr.InitiationTimestamp;
6 voiceCall.endTime = ctr.DisconnectTimestamp;
7 voiceCall.parentCallIdentifier = ctr.PreviousContactId;
8
9 if (ctr.Agent) {
10 voiceCall.acceptTime = ctr.Agent.ConnectedToAgentTimestamp;
11 voiceCall.totalHoldDuration = ctr.Agent.CustomerHoldDuration;
12 voiceCall.longestHoldDuration = ctr.Agent.LongestHoldDuration;
13 voiceCall.agentInteractionDuration = ctr.Agent.AgentInteractionDuration;
14 voiceCall.numberOfHolds = ctr.Agent.NumberOfHolds;
15 }
16
17 if (ctr.Queue) {
18 voiceCall.enqueueTime = ctr.Queue.EnqueueTimestamp;
19 voiceCall.queue = ctr.Queue.Name;
20 }
21
22 if (ctr.InitiationMethod) {
23 voiceCall.initiationMethod = ctr.InitiationMethod;
24 if (ctr.InitiationMethod === 'OUTBOUND') {
25 if (ctr.SystemEndpoint) {
26 voiceCall.fromNumber = ctr.SystemEndpoint.Address;
27 }
28 if ( ctr.CustomerEndpoint) {
29 voiceCall.toNumber = ctr.CustomerEndpoint.Address;
30 }
31 } else {
32 if (ctr.SystemEndpoint) {
33 voiceCall.toNumber = ctr.SystemEndpoint.Address;
34 }
35 if ( ctr.CustomerEndpoint) {
36 voiceCall.fromNumber = ctr.CustomerEndpoint.Address;
37 }
38 }
39 }
40
41 if (ctr.Recording) {
42 voiceCall.recordingLocation = ctr.Recording.Location;
43 }
44
45 // Check if there are custom contact attributes
46 if (ctr.Attributes) {
47 var callAttributes = {};
48
49 // Get contact attributes data into call attributes
50 callAttributes = getCallAttributes(ctr.Attributes);
51
52 voiceCall.callAttributes = callAttributes;
53 }
54
55 Object.keys(voiceCall).forEach(function (key) {
56 if (voiceCall[key] === null || voiceCall[key] === undefined) {
57 delete voiceCall[key];
58 }
59 });
60
61 return {contactId: ctr.ContactId, fields: voiceCall};
62}
63
64// COPIED function to filter call attributes
65/**
66 * Filter call attributes to be included in API payload based on prefix and strip prefix
67 *
68 * @param {object} rawCallAttributes - Contact flow attributes
69 *
70 * @return {string} - Stringified contact flow attributes with prefix removed
71 */
72function getCallAttributes(rawCallAttributes) {
73 const prefix = 'sfdc-';
74 const prefixLen = prefix.length;
75 let callAttributes = {};
76
77 for (const key in rawCallAttributes) {
78 if (rawCallAttributes.hasOwnProperty(key) && key.startsWith(prefix)) {
79 callAttributes[key.substring(prefixLen)] = rawCallAttributes[key];
80 }
81 // Set SCV Limits Error if the specific contact attribute is set
82 if (rawCallAttributes.sf_realtime_transcription_status) {
83 callAttributes.sf_realtime_transcription_status = rawCallAttributes.sf_realtime_transcription_status;
84 }
85 }
86
87 return JSON.stringify(callAttributes);
88}
89
90// UPDATED: Export the 2 new functions (transformCTR, getCallAttributes)
91module.exports = {
92 generateJWT,
93 getAccessToken,
94 formatObjectApiName,
95 getSObjectFieldValuesFromConnectLambdaParams,
96 transformCTR,
97 getCallAttributes
98};Step 6: Call the New Method on the InvokeSalesforceRestApiFunction Lambda
Call this new method in InvokeSalesforceRestApiFunction by specifying a methodName of retryCTRSync and a soql attribute of SELECT Id, VendorCallKey FROM VoiceCall WHERE CustomerHoldDuration = null. You can schedule this call to run periodically as documented in Amazon’s documentation: Schedule AWS Lambda Functions Using CloudWatch Events.
Test This Example
To test this example:
- Dial your phone number in order to create a VoiceCall record. After the call is answered, hang up.
- In your org, find the newly created VoiceCall 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 such as
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 VoiceCall record now has the CTR fields filled in.