3

I'm gettin

You have uncommitted work pending. Please commit or rollback before calling out..

Exception while Running a Test of the class i will attach. This class is invoked by onTask Trigger (AfterInsert)

But the thing is that this is a replicate of what i do on leads and it works perfectly. I can't se what's that pending job. I've read other post related to this problem but my callout is in a different method from the inserts and selects from the database, so i'm probably missing something pretty basic but I just can't figure it out.

I commented Account Triggers so they don't affect to finding the root of the problem, same with onTask Triggers but TriggerTaskSendSalesInteractiontoGA. I also checked that there are no email Workflows involved on this situation. So I don't know what else to look on.

Class:

public with sharing class TriggerTaskSendSalesInteractiontoGA implements Triggers.Handler {

    public static Id FOLDER_ID = [SELECT Id FROM Folder WHERE DeveloperName='google_analyticsWS' LIMIT 1].Id;
    public void handle() {
        List <String> TasksToSend = new List <String> ();
        Map<id,Task> MapNewTasks = new Map<id,Task> ((List<Task>)Trigger.new);
        Map<id,id> MapLeadWithTask = new Map<id,id> (); // <TaskID,LeadID> 
        Map<id,id> MapAccountWithTask = new Map<id,id> (); // <TaskID,AcountID>
        Set<id> whoIds = new Set<id> ();
        Set<id> whatIds = new Set<id> ();
        for(Task T: (List <Task>)Trigger.new) {
            if((''+T.whoid).startswith('00Q')) {
                MapLeadWithTask.put(T.id,T.whoid);
            } 
            if((''+T.whatid).startswith('001')) {
                MapAccountWithTask.put(T.id,T.whatid);
            }
        }
        List <Lead> Leadsinvonved = [SELECT id,analytics_id__c,FxRatingLetter__C,Lead_Status_GA__c,Leadsource FROM LEAD WHERE id in: MapLeadWithTask.values()];
        Map<id,Lead> MapLeadsInvolved = new Map <id,Lead> (Leadsinvonved);
        LIst <Account> AccountsInvolved = [SELECT id,analytics_id__c,FXRatingLetter__c,AccountSource,Account_status__c FROM Account WHERE id in: MapAccountWithTask.values()];
        Map<id,Account> MapAccountsInvolved = new Map <id,Account> (AccountsInvolved);
        for(Task T:(List<Task>)Trigger.new) {
            if(MapLeadWithTask.keyset().contains(T.id)) {
                Lead LeadToEvaluate = MapLeadsInvolved.get(MapLeadWithTask.get(T.id));
                if(LeadToEvaluate.analytics_id__c!='') { // IF the Account has analytics ID and we detect that some of the criteria to be informed to GA has changed we serialized in a Json List
                    TasksToSend.add(JSON.serialize(new TaskHelper(T,LeadToEvaluate)));
                }   
            }
            if(MapAccountWithTask.keyset().contains(T.id)) {
                Account AccountToEvaluate = MapAccountsInvolved.get(MapAccountWithTask.get(T.id));
                if(AccountToEvaluate.analytics_id__c!='') { // IF the Account has analytics ID and we detect that some of the criteria to be informed to GA has changed we serialized in a Json List
                    TasksToSend.add(JSON.serialize(new TaskHelper(T,AccountToEvaluate)));
                }
            }   
        }   
        sendInfoToGA(TasksToSend);
    }
    public with sharing Class TaskHelper {
        public String FXRating {get;set;}
        public String Status {get;set;}
        public String AnalyticsId {get;set;}
        public String AccountBussinessID {get;set;}
        public String TaskType {get;set;}
        public String source {get;set;}

        TaskHelper(Task T, Account A){
            AccountBussinessID = A.Id;
            FXRating = A.FXRatingLetter__c;
            Status = A.Account_status__c;
            AnalyticsId = A.analytics_id__c;
            TaskType = T.type;
            Source = A.AccountSource;
        }
        TaskHelper(Task T, Lead L){
            AccountBussinessID = '';
            FXRating = L.FXRatingLetter__c;
            Status = L.Lead_Status_GA__c;
            AnalyticsId = L.analytics_id__c;
            TaskType = T.type;
            Source =  L.LeadSource;
        }
    } 

    @future(callout=true)
    private static void sendInfoToGA (List<String> TasksToSend) {
        TaskHelper TaskToSend;      
        while(!TasksToSend.isEmpty()) {
            // We will generate a batch of 20 hits till we send all new Info collected
            String endPoint = 'http://www.google-analytics.com/batch?';
            Http http        = new Http();
            HttpRequest req  = new HttpRequest();
            HttpResponse res = new HttpResponse();
            String body = '';
            req.setHeader('Content-Length', '0');
            req.setMethod('POST');
            //Control chunks of 20  HITS
            for(integer i=0; i<20 && !TasksToSend.isEmpty(); ++i) {
                TaskToSend = (TaskHelper) JSON.deserialize(TasksToSend[0], TaskHelper.class);
                String analyticsId = TaskToSend.AnalyticsId;
                body += 'v=1&tid=UA-XXXXXXXX-X&t=event&cid='+analyticsId+'&dp=/salesForce&ec=Offline_Interaction&ea=Sales_Interaction&el='+TaskToSend.TaskType+'&ni=1&cs=direct&cm=none&cd1='+analyticsId+'&cd2='+String.valueOf((integer)(Math.random()*1000));
                iF(TaskToSend.AccountBussinessID=='') {
                    body+=(TaskToSend.AccountBussinessID!=NUll && TaskToSend.AccountBussinessID.startswith('001'))? '&cd3='+ TaskToSend.AccountBussinessID:'';
                    body+=(TaskToSend.Status!=null)?'&cd4='+TaskToSend.Status.deleteWhiteSpace():'';
                    body+=(TaskToSend.FXRating!=null)?'&cd5='+TaskToSend.FXRating.deleteWhiteSpace()+'\n\n':'\n\n';

                }
                else {
                    body+='&cd3=';
                    body+=(TaskToSend.Status!=null)?'&cd6='+TaskToSend.Status.deleteWhiteSpace():'';
                    body+=(TaskToSend.FXRating!=null)?'&cd7='+TaskToSend.FXRating.deleteWhiteSpace():'';
                }
                body+=(TasktoSend.Source!=null)? '&cd9='+TaskToSend.Source.deleteWhiteSpace()+'\n\n':'\n\n';           

                TaskstoSend.remove(0);
            }
            try { // IF we detect some kind of exception we will notify with an email to the administrator.
                req.setBody(body);
                req.setEndpoint(endPoint);
                res = http.send(req);
                UtilsWS.createDocument('LastSalesActivitToGaExport'+System.now().addhours(2)+Math.random(),res.toString()+'\n'+req.getEndpoint()+'\n'+req.getBody(),FOLDER_ID);
            } 
            Catch(System.CalloutException e) {
                System.debug('EXCEPTION : ' + e);
                Util.sendExceptionEmail('Exception on Callout to Google Analytics', e);             
            }
        }
    } }

Test :

@isTest static void test_method_with_Account() {
        Account A = new Account(Annual_FX__C = 9000000,Name='Tech',AccountSource='Content',Analytics_id__c='123456789.987654321',Account_Status__c='PreApproved');
        insert A;     
        Task T = new Task(Whatid=A.id,Type='Call',ActivityDate=System.today().addDays(1));
        Test.setMock(HttpCalloutMock.class, new CalloutGAMock(200,'OK','System.HttpResponse[Status=OK, StatusCode=200]'));
        insert T;
4

If your mock response fails, you have this section of code that follows:

        try { // IF we detect some kind of exception we will notify with an email to the administrator.
            req.setBody(body);
            req.setEndpoint(endPoint);
            res = http.send(req);
            UtilsWS.createDocument('LastSalesActivitToGaExport'+System.now().addhours(2)+Math.random(),res.toString()+'\n'+req.getEndpoint()+'\n'+req.getBody(),FOLDER_ID);
        } 
        Catch(System.CalloutException e) {
            System.debug('EXCEPTION : ' + e);
            Util.sendExceptionEmail('Exception on Callout to Google Analytics', e);             
        }

When you call Util.sendExceptionEmail, that's another DML operation. If you do the try operation, you need to have a mock response to it. My guess is that your initial mock response is failing and thus you're entering your try-catch block, leaving unfinished work. (you'll want to test that anyway in a separate method)

I recommend you add debug statements to your code to see what's happening when your test class runs.

Edit

After taking another look at your code, I have a couple suggestions that may help you resolve your issues. I would suggest that you consider refactoring your code. Currently you have your

  • outer class declaration
  • static variable initialization via a query
  • (no constructor)
  • Handler method
  • inner helper class with two inner methods inside it
  • @future method

Your handle method repeatedly calls the inner helper class while within a for loop then sends the results to your @future method.

Your @future method takes the results and puts those results into a While loop. It declares and establishes a new new HttpRequest() and new HttpResponse(), etc for each batch of 20 records. It then goes into a for loop that deserializes what you serialized earlier in your handle method, apparently sorting them and removing any extra whitespace as it assembles the body.

After the loop ends where you assemble the body, you begin your try-catch block. After you've made your request and get your results returned, you call UtilsWS.createDocument. If the name is what it implies, it would appear that you're creating a record of some kind, presumably to track success or failure of your request. That's a DML operation.

If there's an exception, you go to your catch block where you send an email, again another DML operation.

I'm thinking that perhaps there are two things that may need to be done here to refactor your code.

First, is that these probably need to be split into 3 separate classes. The first would contain what's currently:

  • outer class declaration
  • static variable initialization via a query
  • (no constructor)
  • Handler method

Your next class would be the contents of your inner helper class with it's two inner methods, but as a class all of its own.

Finally, the @future method would be put into a 3rd class so it can be called separately, but there's something beyond that which probably needs to be done with the latter.

I suspect the try-catch block needs to be taken out of the While loop. I think the best way to do that would be doing something like this:

@future(callout=true)
private static void sendInfoToGA (List<String> TasksToSend) {
    TaskHelper TaskToSend;      
    while(!TasksToSend.isEmpty()) {
        // We will generate a batch of 20 hits till we send all new Info collected
        // define body above as an instance variable so both methods can use it
        String body = '';
        //Control chunks of 20  HITS
        for(integer i=0; i<20 && !TasksToSend.isEmpty(); ++i) {
            TaskToSend = (TaskHelper) JSON.deserialize(TasksToSend[0], TaskHelper.class);
            String analyticsId = TaskToSend.AnalyticsId;
            body += 'v=1&tid=UA-XXXXXXXX-X&t=event&cid='+analyticsId+'&dp=/salesForce&ec=Offline_Interaction&ea=Sales_Interaction&el='+TaskToSend.TaskType+'&ni=1&cs=direct&cm=none&cd1='+analyticsId+'&cd2='+String.valueOf((integer)(Math.random()*1000));
            iF(TaskToSend.AccountBussinessID=='') {
                body+=(TaskToSend.AccountBussinessID!=NUll && TaskToSend.AccountBussinessID.startswith('001'))? '&cd3='+ TaskToSend.AccountBussinessID:'';
                body+=(TaskToSend.Status!=null)?'&cd4='+TaskToSend.Status.deleteWhiteSpace():'';
                body+=(TaskToSend.FXRating!=null)?'&cd5='+TaskToSend.FXRating.deleteWhiteSpace()+'\n\n':'\n\n';

            }
            else {
                body+='&cd3=';
                body+=(TaskToSend.Status!=null)?'&cd6='+TaskToSend.Status.deleteWhiteSpace():'';
                body+=(TaskToSend.FXRating!=null)?'&cd7='+TaskToSend.FXRating.deleteWhiteSpace():'';
            }
            body+=(TasktoSend.Source!=null)? '&cd9='+TaskToSend.Source.deleteWhiteSpace()+'\n\n':'\n\n';           

            TaskstoSend.remove(0);
        }
        // now call the actual send method
        sendBody(body);


    private static void sendBody (String body) {
        this.body = body;
        String endPoint = 'http://www.google-analytics.com/batch?';
        Http http        = new Http();
        HttpRequest req  = new HttpRequest();
        HttpResponse res = new HttpResponse();
        req.setHeader('Content-Length', '0');
        req.setMethod('POST');
        Boolean Success = false;
        Boolean SendEmail = false;
        try { // IF we detect some kind of exception we will notify with an email to the administrator.
            req.setBody(body);
            req.setEndpoint(endPoint);
            res = http.send(req);
            // Collect records here with this info, but do not send it.
            // if request is successful, Success = true;
         } 
        Catch(System.CalloutException e) {
            System.debug('EXCEPTION : ' + e);
            // collect records here, but do not send it
            // if exception SendEmail = true;                          
        }

        if(Success == true){
           UtilsWS.createDocument('LastSalesActivitToGaExport'+System.now().addhours(2)+Math.random(),res.toString()+'\n'+req.getEndpoint()+'\n'+req.getBody(),FOLDER_ID);
        {
        if(SendEmail == true){
           Util.sendExceptionEmail('Exception on Callout to Google Analytics', e);
        }
    }
} 
  • The thing actually it's that the exception i'm getting that makes me enter the catch its the one we're talking about. [You have uncommitted work pending. Please commit or rollback before calling out]. I put a System.debug and verified that the value was correct 16:43:09.89 (2094139875)|USER_DEBUG|[99]|DEBUG|RES System.HttpResponse[Status=OK, StatusCode=200] – Alexander Aeons Torn Apr 7 '16 at 14:46
  • Okey, after some System.debugs more, the first exception(the one we're discussing, it's generated on theres = http.send(req); line. Then when we catch it we try to invoke Util.SendEmail and it generates another Exception NO_MASS_MAIL_PERMISSION, Single email is not enabled for your organization or profile.: [] But I still don't manage to pull out why is that line of code generating the exception since if we later do a system.debug of res the result is correct as I stated on the previous comment. – Alexander Aeons Torn Apr 7 '16 at 15:04
  • My instincts would say it's because you have an unhandled exception. I also see a reference to UtilsWS.createDocument. Perhaps there's a DML operation there as well? You seem to have additional classes involved here too that aren't shown plus permissions issues involved that are complicating giving you an answer. Clearly if your user profile can't send the email, it's not the right one to be testing with. – crmprogdev Apr 7 '16 at 17:35
  • The Email Exception can be solved by just changing deliverability options to Allmail. And actually since the first exception is catched i'm not getting error on the test. Since I tested manually the code and it works I could cheat the deploy since my code coverage its 96% or so but I still would like to find out what the hell is going on. – Alexander Aeons Torn Apr 8 '16 at 9:14
  • See my edits. This puts all of your DML at end of your code per link in @MihalNeagoe answer. – crmprogdev Apr 8 '16 at 14:47
3

Doing a quick google search I found this post

  • callout
  • insert
  • callout <---- fails here

Make sure you do all the DML calls before doing a callout in the test

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.