Newer Version Available

This content describes an older version of this product. View Latest

Retry CTR Sync for VoiceCall Records

Typically, Call Trace Record (CTR) data is automatically stored in VoiceCall records. However, there are occasions where this sync doesn’t occur, and it isn’t always possible to access CTR data in Amazon Connect after the call. This example shows you how to back up CTR data to a separate S3 bucket, then check for VoiceCall records that don’t have CTR data, and then resync the CTR 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 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

To run this example:
  1. Have your AWS root user or AWS administrator credentials ready.
  2. Be familiar with Amazon S3 buckets. To learn more, see Amazon’s documentation: Creating and configuring an S3 bucket.
  3. Be familiar with modifying Amazon Connect Lambda functions. To learn more, see Amazon’s documentation: AWS Lambda Developer Guide.
  4. Configure the OAuth support for the InvokeSalesforceRestApiFunction Lambda function. This is required to support the getAccessToken function.

Step 1: Create an S3 Bucket

Create an S3 bucket within the region and set the access policies to be accessible within the region where your Amazon instance is configured. Also, ensure you set the right access control policies for the bucket so it's not accessible outside of your instance. To learn more, see Amazon’s documentation: Creating and configuring 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:

  1. 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
  2. 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.
  3. Update VoiceCall records in the org. Update VoiceCall records based on the information returned from the previous step.

This code calls the InvokeTelephonyIntegrationApiFunction Lambda. You can make this call in one of the following two ways:

a) Declare an environment variable for that Lambda and reference it from your code.

b) Hard-code the ARN of the InvokeTelephonyIntegrationApiFunction Lambda into your code.

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.

Important

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 (var 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    var 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:

  1. Dial your phone number in order to create a VoiceCall record. After the call is answered, hang up.
  2. In your org, find the newly created VoiceCall record and remove some CTR fields, including CustomerHoldDuration.
  3. 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}
  4. Check that the VoiceCall record now has the CTR fields filled in.