Search code examples
javascriptnode.jsbotframeworkdirect-line-botframeworkweb-chat

UserState showing null in onMembersAdded function


I have logic in my onMembersAdded function to load the user state and see if userData.accountNumber attribute exists. If it does not, a run an auth dialog to get the user's account number. If the attribute does exist, the welcome message should be displayed without a prompt.

When I test on local, this works fine. But when I test on Azure, I always end up in the !userData.accountNumber block. Through checking the console log, I can see that in the onMembersAdded function is showing {} for the userData object. But in auth dialog, even if I skip the prompt (which we allow the user to do), the accountNumber attribute is there in userState (if it had been entered previously).

The only thing I can figure is that somehow using BlobStorage for state, as I do on Azure, is somehow exhibiting different behavior than MemoryStorage which I am using for local testing. I thought it might be a timing issue, but I am awaiting the get user state call, and besides if I do enter an account number in the auth dialog, the console log immediately following the prompt shows the updated account number, no problem.

EDIT: From the comments below, it's apparent that the issue is the different way channels handle onMembersAdded. It seems in emulator both bot and user are added at the same time, but on webchat/directline, user isn't added until the first message is sent. So that is the issue I need a solution to.

Here is the code in the constructor defining the state variables and onMembersAdded function:


// Snippet from the constructor. UserState is passed in from index.js

// Create the property accessors
this.userDialogStateAccessor = userState.createProperty(USER_DIALOG_STATE_PROPERTY);
this.dialogState = conversationState.createProperty(DIALOG_STATE_PROPERTY);

// Create local objects
this.conversationState = conversationState;
this.userState = userState;

        this.onMembersAdded(async (context, next) => {
            const membersAdded = context.activity.membersAdded;

            for (let member of membersAdded) {
                if (member.id === context.activity.recipient.id) {
                    this.appInsightsClient.trackEvent({name:'userAdded'});

                    // Get user state. If we don't have the account number, run an authentication dialog
                    // For initial release this is a simple prompt
                    const userData = await this.userDialogStateAccessor.get(context, {});
                    console.log('Members added flow');
                    console.log(userData);
                    if (!userData.accountNumber) {
                        console.log('In !userData.accountNumber block');
                        const dc = await this.dialogs.createContext(context);
                        await dc.beginDialog(AUTH_DIALOG);
                        await this.conversationState.saveChanges(context);
                        await this.userState.saveChanges(context);
                    } else {
                        console.log('In userData.accountNumber block');
                        var welcomeCard = CardHelper.GetHeroCard('',welcomeMessage,menuOptions);
                        await context.sendActivity(welcomeCard);
                        this.appInsightsClient.trackEvent({name:'conversationStart', properties:{accountNumber:userData.accountNumber}});
                    }
                }
            }

            // By calling next() you ensure that the next BotHandler is run.
            await next();
        });

Solution

  • If you want your bot to receive a conversation update from Web Chat with the correct user ID before the user sends a message manually, you have two options:

    1. Instead of connecting to Direct Line with a secret, connect with a token (recommended). Note that this will only work if you provide a user property in the body of your Generate Token request.
    2. Have Web Chat send an initial activity to the bot automatically so the user doesn't have to. This would be in response to DIRECT_LINE/CONNECT_FULFILLED, and it could be an invisible event activity so to the user it still looks like the first activity in the conversation came from the bot.

    If you go with option 1, your bot will receive one conversation update with both the bot and the user in membersAdded, and the from ID of the activity will be the user ID. This is ideal because it means you will be able to acess user state.

    If you go with option 2, your bot will receive two conversation update activities. The first is the one you're receiving now, and the second is the one with the user ID that you need. The funny thing about that first conversation update is that the from ID is the conversation ID rather than the bot ID. I presume this was an attempt on Web Chat's part to get the bot to mistake it for the user being added, since Bot Framework bots typically recognize that conversation update by checking if the from ID is different from the member being added. Unfortunately this can result in two welcome messages being sent because it's harder to tell which conversation update to respond to.

    Conversation updates have been historically unreliable in Web Chat, as evidenced by a series of GitHub issues. Since you may end up having to write channel-aware bot code anyway, you might consider having the bot respond to a backchannel event instead of a conversation update when it detects that the channel is Web Chat. This is similar to option 2 but you'd have your bot actually respond to that event rather than the conversation update that got sent because of the event.