@opentelemetry/instrumentation-cucumber
Version:
OpenTelemetry instrumentation for `@cucumber/cucumber` automated tests runner
294 lines • 13.5 kB
JavaScript
"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