UNPKG

@opentelemetry/instrumentation-cucumber

Version:
294 lines 13.5 kB
"use strict"; /* * Copyright The OpenTelemetry Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CucumberInstrumentation = void 0; const api_1 = require("@opentelemetry/api"); const instrumentation_1 = require("@opentelemetry/instrumentation"); const semantic_conventions_1 = require("@opentelemetry/semantic-conventions"); const types_1 = require("./types"); /** @knipignore */ const version_1 = require("./version"); const hooks = ['Before', 'BeforeStep', 'AfterStep', 'After']; const steps = ['Given', 'When', 'Then']; const supportedVersions = ['>=8.0.0 <13']; class CucumberInstrumentation extends instrumentation_1.InstrumentationBase { module; constructor(config = {}) { super(version_1.PACKAGE_NAME, version_1.PACKAGE_VERSION, config); } init() { return [ new instrumentation_1.InstrumentationNodeModuleDefinition('@cucumber/cucumber', supportedVersions, (moduleExports) => { this.module = moduleExports; steps.forEach(step => { if ((0, instrumentation_1.isWrapped)(moduleExports[step])) { this._unwrap(moduleExports, step); } this._wrap(moduleExports, step, this._getStepPatch(step)); }); hooks.forEach(hook => { if ((0, instrumentation_1.isWrapped)(moduleExports[hook])) { this._unwrap(moduleExports, hook); } this._wrap(moduleExports, hook, this._getHookPatch(hook)); }); return moduleExports; }, (moduleExports) => { if (moduleExports === undefined) return; [...hooks, ...steps].forEach(method => { this._unwrap(moduleExports, method); }); }, [ new instrumentation_1.InstrumentationNodeModuleFile('@cucumber/cucumber/lib/runtime/test_case_runner.js', supportedVersions, moduleExports => { if ((0, instrumentation_1.isWrapped)(moduleExports.default.prototype.run)) { this._unwrap(moduleExports.default.prototype, 'run'); this._unwrap(moduleExports.default.prototype, 'runStep'); if ('runAttempt' in moduleExports.default.prototype) { this._unwrap(moduleExports.default.prototype, 'runAttempt'); } } this._wrap(moduleExports.default.prototype, 'run', this._getTestCaseRunPatch()); this._wrap(moduleExports.default.prototype, 'runStep', this._getTestCaseRunStepPatch()); if ('runAttempt' in moduleExports.default.prototype) { this._wrap(moduleExports.default.prototype, 'runAttempt', this._getTestCaseRunAttemptPatch()); } return moduleExports; }, moduleExports => { if (moduleExports === undefined) return; this._unwrap(moduleExports.default.prototype, 'run'); this._unwrap(moduleExports.default.prototype, 'runStep'); if ('runAttempt' in moduleExports.default.prototype) { this._unwrap(moduleExports.default.prototype, 'runAttempt'); } }), ]), ]; } static mapTags(tags) { return tags.map(tag => tag.name); } static setSpanToError(span, error) { span.recordException(error); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error?.message ?? error, }); } setSpanToStepStatus(span, status, context) { // if the telemetry is enabled, the module should be defined if (!this.module) return; span.setAttribute(types_1.AttributeNames.STEP_STATUS, status); if ([ this.module.Status.UNDEFINED, this.module.Status.AMBIGUOUS, this.module.Status.FAILED, ].includes(status)) { span.recordException(status); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: context || status, }); } } _getTestCaseRunPatch() { const instrumentation = this; return function (original) { return async function (...args) { const gherkinDocument = this['gherkinDocument']; const { feature } = gherkinDocument; const pickle = this['pickle']; const scenario = feature.children.find(node => node?.scenario?.id === pickle.astNodeIds[0])?.scenario; return instrumentation.tracer.startActiveSpan(`Feature: ${feature.name}. Scenario: ${pickle.name}`, { kind: api_1.SpanKind.CLIENT, attributes: { [semantic_conventions_1.ATTR_CODE_FILE_PATH]: gherkinDocument.uri, [semantic_conventions_1.ATTR_CODE_LINE_NUMBER]: scenario.location.line, [semantic_conventions_1.ATTR_CODE_FUNCTION_NAME]: `${feature.name} ${scenario.name}`, [types_1.AttributeNames.FEATURE_TAGS]: CucumberInstrumentation.mapTags(feature.tags), [types_1.AttributeNames.FEATURE_LANGUAGE]: feature.language, [types_1.AttributeNames.FEATURE_DESCRIPTION]: feature.description, [types_1.AttributeNames.SCENARIO_TAGS]: CucumberInstrumentation.mapTags(scenario.tags), [types_1.AttributeNames.SCENARIO_DESCRIPTION]: scenario.description, }, }, async (span) => { try { const status = await original.apply(this, args); instrumentation.setSpanToStepStatus(span, status); return status; } catch (error) { CucumberInstrumentation.setSpanToError(span, error); throw error; } finally { span.end(); } }); }; }; } _getTestCaseRunStepPatch() { const instrumentation = this; return function (original) { return async function (...args) { const [pickleStep] = args; return instrumentation.tracer.startActiveSpan(pickleStep.text, { kind: api_1.SpanKind.CLIENT, attributes: { [types_1.AttributeNames.STEP_TYPE]: pickleStep.type, }, }, async (span) => { try { const runStepResult = await original.apply(this, args); const { result, error } = (() => { if ('result' in runStepResult) { return runStepResult; } return { result: runStepResult, error: undefined, }; })(); instrumentation.setSpanToStepStatus(span, result.status, result.message); if (error) { CucumberInstrumentation.setSpanToError(span, error); } return runStepResult; } catch (error) { CucumberInstrumentation.setSpanToError(span, error); throw error; } finally { span.end(); } }); }; }; } _getTestCaseRunAttemptPatch() { const instrumentation = this; return function (original) { return async function (...args) { const [attempt] = args; return instrumentation.tracer.startActiveSpan(`Attempt #${attempt}`, { kind: api_1.SpanKind.CLIENT, attributes: {}, }, async (span) => { try { const result = await original.apply(this, args); const worstResult = this.getWorstStepResult(); instrumentation.setSpanToStepStatus(span, worstResult.status, worstResult.message); return result; } catch (error) { CucumberInstrumentation.setSpanToError(span, error); throw error; } finally { span.end(); } }); }; }; } _getHookPatch(name) { const instrumentation = this; return function (original) { return function (tagsOrOptions, code) { if (typeof tagsOrOptions === 'function') { code = tagsOrOptions; tagsOrOptions = {}; } function traceableCode(arg) { // because we're wrapping the function that was passed to the hook, // it will stay wrapped in cucumber's internal state // even if we disable the instrumentation if (!instrumentation.isEnabled()) return code?.call(this, arg); return instrumentation.tracer.startActiveSpan(name, { kind: api_1.SpanKind.CLIENT, }, async (span) => { try { return await code?.call(this, arg); } catch (error) { this.attach?.(JSON.stringify(span.spanContext())); CucumberInstrumentation.setSpanToError(span, error); throw error; } finally { span.end(); } }); } return original.call(this, tagsOrOptions, traceableCode); }; }; } _getStepPatch(name) { const instrumentation = this; return function (original) { return function (pattern, options, code) { if (typeof options === 'function') { code = options; options = {}; } function traceableCode(...args) { // because we're wrapping the function that was passed to the hook, // it will stay wrapped in cucumber's internal state // even if we disable the instrumentation if (!instrumentation.isEnabled()) return code?.apply(this, args); return instrumentation.tracer.startActiveSpan(`${name}(${pattern.toString()})`, { kind: api_1.SpanKind.CLIENT, // ignore the last argument because it's a callback attributes: args.slice(0, -1).reduce((attrs, arg, index) => ({ ...attrs, [`${types_1.AttributeNames.STEP_ARGS}[${index}]`]: arg?.raw instanceof Function ? JSON.stringify(arg.raw()) : arg, }), {}), }, async (span) => { try { return await code?.apply(this, args); } catch (error) { this.attach?.(JSON.stringify(span.spanContext())); CucumberInstrumentation.setSpanToError(span, error); throw error; } finally { span.end(); } }); } // cucumber asks for the number of arguments to match the specified pattern // copy the value from the original function Object.defineProperty(traceableCode, 'length', { value: code?.length, }); return original.call(this, pattern, options, traceableCode); }; }; } } exports.CucumberInstrumentation = CucumberInstrumentation; //# sourceMappingURL=instrumentation.js.map