virtual-alexa icon indicating copy to clipboard operation
virtual-alexa copied to clipboard

Expose the lambda context in the virtual alexa filter() method

Open OpenDog opened this issue 6 years ago • 6 comments

The ability to tweak the lambda context as well in VirtualAlexa (filter function) would be a nice feature. Currently it only offers the request (which is the alexa payload in case of BST tools).

Use case

Our lambda is exposed with the AWS API GW (with Serverless) and it serves multiple skills, both on Alexa and Google Assistant and it relies on the request path to figure out the platform and the skill.

We use the BST proxy for debugging, and the Virtual Alexa for unit testing. The BST proxy puts the request path and the query parameters into the lambda context (request attribute). We need to mock that with VirtualAlexa in the unit tests.

But there is a bigger picture...

It would be really cool if the BST tools would offer an option to simulate the event and context format of the API Gateway Lambda Proxy. This is default with Serverless or ClaudiaJS installations. The important difference is that Amazon uses the event to pass in information about the HTTP request (BST sends the payload in the event). Also Amazon's callback response format is different too: they want you to wrap the response into a simple object.

The bottom line is this: the consolidation of the lambda event and context structures would make the code simpler for lambda projects exposed with Serverless or ClaudiaJS (anything that uses the default API GW Lambda proxy format).

To illustrate the "lambda context hell" we are in, this is what we call first thing in the lambda handler to sniff out the environment:

import * as fs from "fs";

/**
 * Pick out the data we need, depending on the environment we run in.
 * Currently we support AWS default lambda proxy, bst proxy (real-time debugging) 
 * and VirtualAlexa (unit testing).
 *
 * Additionally attach a function to build the response in a way that is specific to the
 * lambda environment.
 *
 * @param event
 * @param context
 * @returns {any}
 */
export function translateLambdaContext(event: any, context: any): any {
    if (!event || !context) { return {}; }

    let eventContext = {};

    if (event.requestContext) {
        eventContext = lambdaProxyContext(event, context);
    } else if (context.request) {
        eventContext = bstContext(event, context);
    } else if (event.testContext) {
        eventContext = virtualBstContext(event, context);
    }

    return eventContext;
}

/**
 * BST format
 *
 * @param lambdaEvent
 * @param lambdaContext
 * @returns {any}
 */
function bstContext(lambdaEvent: any, lambdaContext: any): any {
    const [path] = lambdaContext.request.url.split("?");

    const params = Object.assign({}, parsePath(path));
    params.rawBody = JSON.stringify(lambdaEvent);
    params.body = lambdaEvent;

    params.buildResponse = (code: number, result: any): any => {
        return result;
    };

    return params;
}

/**
 * Virtual BST format
 *
 * The path is piggybacked on the payload (lambda event) for now.
 *
 * @param lambdaEvent
 * @param lambdaContext
 * @returns {any}
 */
function virtualBstContext(lambdaEvent: any, lambdaContext: any): any {
    const path = lambdaEvent.testContext.path;

    const params = Object.assign({}, parsePath(path));
    params.rawBody = JSON.stringify(lambdaEvent);
    params.body = lambdaEvent;

    params.buildResponse = (code: number, result: any): any => {
        return result;
    };

    return params;
}

/**
 * Basic AWS API Gateway lambda proxy format
 *
 * @param lambdaEvent
 * @returns {any}
 */
function lambdaProxyContext(lambdaEvent: any, lambdaContext: any): any {
    if (!lambdaEvent.path) {
        return {};
    }

    const path = lambdaEvent.path;

    const params = Object.assign({}, parsePath(path));
    params.rawBody = lambdaEvent.body;
    params.body = JSON.parse(lambdaEvent.body);
    params.headers = lambdaEvent.headers;
    params.alexaApplicationId =
        lambdaEvent.queryStringParameters ? 
           lambdaEvent.queryStringParameters.alexaApplicationId : undefined;

    params.buildResponse = (code: number, result: any): any => {
        return {
            statusCode: code,
            body: JSON.stringify(result)
        };
    };

    return params;
}

/**
 * This follows the current path convention: .../dev/apps/{appId}/run/{platform}
 *
 * @param {string} path
 * @returns {any}
 */
function parsePath(path: string): any {
    const params: any = {};

    const pathParts: any = path.split("/");
    params.platform = pathParts.pop();
    pathParts.pop(); // "/run/"
    params.appId = pathParts.pop();

    return params;
}

OpenDog avatar Jun 07 '18 15:06 OpenDog

So I understand passing the context. That's easy :-)

With regard to sorting out the "context :fire:" - I do understand better the issue based on your description. Thank you for that.

What do you think would be the best way to address this? Here is my stab at it: Add a flag to virtualAlexa called "simulateGateway" that if set does the following: I guess the ideal solution, we would have a flag like “simulateGateway” that:

  1. Pushes the event JSON onto an event.body field
  2. Looks for the Alexa JSON response on a body field of the response So rather than assuming it is the root element, it would take it from the "body" element of the response provided to context.done()

Am I on the right track?

jkelvie avatar Jun 07 '18 19:06 jkelvie

Yes exactly. simulateGateway flag is great. BTW in the response there are more attributes you can use in BST proxy: http response code and return headers.

Maybe another flag to simulate the lambda event and context when the lambda is hooked directly to an Alexa skill (i.e. the event is a vanilla Alexa event - no API GW). I'm not sure what the format is. Or is it the same as the current BST format (event = payload)?

OpenDog avatar Jun 07 '18 19:06 OpenDog

I forgot. for the simulateGateway you also need to push the request info (path, queryString) onto event. And the body is the JSON string not the object (like in BST).

OpenDog avatar Jun 07 '18 19:06 OpenDog

So I understand passing the context. That's easy :-)

I haven't read all of the documentation yet (forgive me please), but can you share how to pass in the context?

Btw, my team and I are loving Bespoken!

dgreene1 avatar Nov 14 '18 15:11 dgreene1

@dgreene1 We meant adding the changes to pass the lambda context. Which we haven't done yet.

But do you mean the same, or do you mean the context used when an alexa session have more than one interaction. If it's the second, the instance keeps the context alive by default

jperata avatar Nov 14 '18 15:11 jperata

Thank you for the quick response @jperata. :) I mean the first. I would like an option to send an event like you can do in the lamda UI tester that Amazon provides.

Additionally, I am interested in what @OpenDog was asking for with the gateway.

dgreene1 avatar Nov 14 '18 15:11 dgreene1