Skip to content

Commit

Permalink
feat: 🎸 store debug information for functions that throw
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Feb 18, 2020
1 parent ce8d2be commit 9e35351
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 8 deletions.
6 changes: 4 additions & 2 deletions src/plugins/expressions/common/ast/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ export interface ExpressionAstFunctionDebug {
rawError?: any | Error;

/**
* Time in milliseconds it took to execute the function.
* Time in milliseconds it took to execute the function. Duration can be
* `undefined` if error happened during argument resolution, because function
* timing starts after the arguments have been resolved.
*/
duration: number;
duration: number | undefined;
}

export type ExpressionAstArgument = string | boolean | number | ExpressionAstExpression;
71 changes: 71 additions & 0 deletions src/plugins/expressions/common/execution/execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,5 +564,76 @@ describe('Execution', () => {
expect(chain[1].debug!.output).toBe(5);
});
});

describe('when expression throws', () => {
const executor = createUnitTestExecutor();
executor.registerFunction({
name: 'throws',
args: {},
help: '',
fn: () => {
throw new Error('foo');
},
});

test('stores debug information up until the function that throws', async () => {
const execution = new Execution({
executor,
ast: parseExpression('add val=1 | throws | add val=3'),
debug: true,
});
execution.start(0);
await execution.result;

const node1 = execution.state.get().ast.chain[0];
const node2 = execution.state.get().ast.chain[1];
const node3 = execution.state.get().ast.chain[2];

expect(typeof node1.debug).toBe('object');
expect(typeof node2.debug).toBe('object');
expect(typeof node3.debug).toBe('undefined');
});

test('stores error thrown in debug information', async () => {
const execution = new Execution({
executor,
ast: parseExpression('add val=1 | throws | add val=3'),
debug: true,
});
execution.start(0);
await execution.result;

const node2 = execution.state.get().ast.chain[1];

expect(node2.debug?.error).toMatchObject({
type: 'error',
error: {
message: '[throws] > foo',
},
});
expect(node2.debug?.rawError).toBeInstanceOf(Error);
});

test('sets .debug object to expected shape', async () => {
const execution = new Execution({
executor,
ast: parseExpression('add val=1 | throws | add val=3'),
debug: true,
});
execution.start(0);
await execution.result;

const node2 = execution.state.get().ast.chain[1];

expect(node2.debug).toMatchObject({
success: false,
input: expect.any(Object),
args: expect.any(Object),
error: expect.any(Object),
rawError: expect.any(Error),
duration: expect.any(Number),
});
});
});
});
});
30 changes: 24 additions & 6 deletions src/plugins/expressions/common/execution/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { createExecutionContainer, ExecutionContainer } from './container';
import { createError } from '../util';
import { Defer } from '../../../kibana_utils/common';
import { RequestAdapter, DataAdapter } from '../../../inspector/common';
import { isExpressionValueError } from '../expression_types/specs/error';
import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error';
import {
ExpressionAstExpression,
ExpressionAstFunction,
Expand All @@ -32,7 +32,7 @@ import {
parseExpression,
} from '../ast';
import { ExecutionContext, DefaultInspectorAdapters } from './types';
import { getType } from '../expression_types';
import { getType, ExpressionValue } from '../expression_types';
import { ArgumentType, ExpressionFunction } from '../expression_functions';
import { getByAlias } from '../util/get_by_alias';
import { ExecutionContract } from './execution_contract';
Expand Down Expand Up @@ -203,11 +203,15 @@ export class Execution<
return createError({ message: `Function ${fnName} could not be found.` });
}

let args: Record<string, ExpressionValue> = {};
let timeStart: number | undefined;

try {
// `resolveArgs` returns an object because the arguments themselves might
// actually have a `then` function which would be treated as a `Promise`.
const { resolvedArgs } = await this.resolveArgs(fnDef, input, fnArgs);
const timeStart: number = this.params.debug ? Date.now() : 0;
args = resolvedArgs;
timeStart = this.params.debug ? Date.now() : 0;
const output = await this.invokeFunction(fnDef, input, resolvedArgs);

if (this.params.debug) {
Expand All @@ -223,9 +227,23 @@ export class Execution<

if (getType(output) === 'error') return output;
input = output;
} catch (e) {
e.message = `[${fnName}] > ${e.message}`;
return createError(e);
} catch (rawError) {
const timeEnd: number = this.params.debug ? Date.now() : 0;
rawError.message = `[${fnName}] > ${rawError.message}`;
const error = createError(rawError) as ExpressionValueError;

if (this.params.debug) {
(link as ExpressionAstFunction).debug = {
success: false,
input,
args,
error,
rawError,
duration: timeStart ? timeEnd - timeStart : undefined,
};
}

return error;
}
}

Expand Down

0 comments on commit 9e35351

Please sign in to comment.