dd-trace
Version:
Datadog APM tracing client for JavaScript
415 lines (371 loc) • 13.5 kB
JavaScript
'use strict'
const { storage } = require('../../datadog-core')
const id = require('../../dd-trace/src/id')
const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper')
const {
TEST_STATUS,
finishAllTraceSpans,
getTestSuitePath,
getTestSuiteCommonTags,
TEST_SOURCE_START,
TEST_CODE_OWNERS,
TEST_SOURCE_FILE,
TEST_PARAMETERS,
TEST_IS_NEW,
TEST_IS_RETRY,
TEST_EARLY_FLAKE_ENABLED,
TELEMETRY_TEST_SESSION,
TEST_RETRY_REASON,
TEST_MANAGEMENT_IS_QUARANTINED,
TEST_MANAGEMENT_ENABLED,
TEST_BROWSER_NAME,
TEST_MANAGEMENT_IS_DISABLED,
TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX,
TEST_HAS_FAILED_ALL_RETRIES,
TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED,
TEST_SESSION_ID,
TEST_MODULE_ID,
TEST_COMMAND,
TEST_MODULE,
TEST_SUITE,
TEST_SUITE_ID,
TEST_NAME,
TEST_IS_RUM_ACTIVE,
TEST_BROWSER_VERSION,
TEST_RETRY_REASON_TYPES,
TEST_IS_MODIFIED,
isModifiedTest
} = require('../../dd-trace/src/plugins/util/test')
const { RESOURCE_NAME } = require('../../../ext/tags')
const { COMPONENT } = require('../../dd-trace/src/constants')
const {
TELEMETRY_EVENT_CREATED,
TELEMETRY_EVENT_FINISHED
} = require('../../dd-trace/src/ci-visibility/telemetry')
const { appClosing: appClosingTelemetry } = require('../../dd-trace/src/telemetry')
class PlaywrightPlugin extends CiPlugin {
static id = 'playwright'
constructor (...args) {
super(...args)
this._testSuites = new Map()
this.numFailedTests = 0
this.numFailedSuites = 0
this.addSub('ci:playwright:test:is-modified', ({
filePath,
modifiedTests,
onDone
}) => {
const testSuite = getTestSuitePath(filePath, this.repositoryRoot)
const isModified = isModifiedTest(testSuite, 0, 0, modifiedTests, this.constructor.id)
onDone({ isModified })
})
this.addSub('ci:playwright:session:finish', ({
status,
isEarlyFlakeDetectionEnabled,
isTestManagementTestsEnabled,
onDone
}) => {
this.testModuleSpan.setTag(TEST_STATUS, status)
this.testSessionSpan.setTag(TEST_STATUS, status)
if (isEarlyFlakeDetectionEnabled) {
this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true')
}
if (this.numFailedSuites > 0) {
let errorMessage = `Test suites failed: ${this.numFailedSuites}.`
if (this.numFailedTests > 0) {
errorMessage += ` Tests failed: ${this.numFailedTests}`
}
const error = new Error(errorMessage)
this.testModuleSpan.setTag('error', error)
this.testSessionSpan.setTag('error', error)
}
if (isTestManagementTestsEnabled) {
this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
}
this.testModuleSpan.finish()
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
this.testSessionSpan.finish()
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
finishAllTraceSpans(this.testSessionSpan)
this.telemetry.count(TELEMETRY_TEST_SESSION, {
provider: this.ciProviderName,
autoInjected: !!getEnvironmentVariable('DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER')
})
appClosingTelemetry()
this.tracer._exporter.flush(onDone)
this.numFailedTests = 0
})
this.addBind('ci:playwright:test-suite:start', (ctx) => {
const { testSuiteAbsolutePath } = ctx
const store = storage('legacy').getStore()
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.rootDir)
const testSourceFile = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
const testSuiteMetadata = getTestSuiteCommonTags(
this.command,
this.frameworkVersion,
testSuite,
'playwright'
)
if (testSourceFile) {
testSuiteMetadata[TEST_SOURCE_FILE] = testSourceFile
testSuiteMetadata[TEST_SOURCE_START] = 1
}
const codeOwners = this.getCodeOwners(testSuiteMetadata)
if (codeOwners) {
testSuiteMetadata[TEST_CODE_OWNERS] = codeOwners
}
const testSuiteSpan = this.tracer.startSpan('playwright.test_suite', {
childOf: this.testModuleSpan,
tags: {
[COMPONENT]: this.constructor.id,
...this.testEnvironmentMetadata,
...testSuiteMetadata
}
})
this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite')
ctx.parentStore = store
ctx.currentStore = { ...store, testSuiteSpan }
this._testSuites.set(testSuiteAbsolutePath, testSuiteSpan)
return ctx.currentStore
})
this.addSub('ci:playwright:test-suite:finish', ({ testSuiteSpan, status, error }) => {
if (!testSuiteSpan) return
if (error) {
testSuiteSpan.setTag('error', error)
testSuiteSpan.setTag(TEST_STATUS, 'fail')
} else {
testSuiteSpan.setTag(TEST_STATUS, status)
}
if (status === 'fail' || error) {
this.numFailedSuites++
}
testSuiteSpan.finish()
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
})
this.addSub('ci:playwright:test:page-goto', ({
isRumActive,
page
}) => {
const store = storage('legacy').getStore()
const span = store && store.span
if (!span) return
if (isRumActive) {
span.setTag(TEST_IS_RUM_ACTIVE, 'true')
if (page) {
const browserVersion = page.context().browser().version()
if (browserVersion) {
span.setTag(TEST_BROWSER_VERSION, browserVersion)
}
const url = page.url()
const domain = new URL(url).hostname
page.context().addCookies([{
name: 'datadog-ci-visibility-test-execution-id',
value: span.context().toTraceId(),
domain,
path: '/'
}])
}
}
})
this.addBind('ci:playwright:test:start', (ctx) => {
const {
testName,
testSuiteAbsolutePath,
testSourceLine,
browserName,
isDisabled
} = ctx
const store = storage('legacy').getStore()
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.rootDir)
const testSourceFile = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
const span = this.startTestSpan(
testName,
testSuiteAbsolutePath,
testSuite,
testSourceFile,
testSourceLine,
browserName
)
if (isDisabled) {
span.setTag(TEST_MANAGEMENT_IS_DISABLED, 'true')
}
ctx.parentStore = store
ctx.currentStore = { ...store, span }
return ctx.currentStore
})
this.addSub('ci:playwright:worker:report', (serializedTraces) => {
const traces = JSON.parse(serializedTraces)
const formattedTraces = []
for (const trace of traces) {
const formattedTrace = []
for (const span of trace) {
const formattedSpan = {
...span,
span_id: id(span.span_id),
trace_id: id(span.trace_id),
parent_id: id(span.parent_id)
}
if (span.name === 'playwright.test') {
// TODO: remove this comment
// TODO: Let's pass rootDir, repositoryRoot, command, session id and module id as env vars
// so we don't need this re-serialization logic. This can be passed just once, since they're unique
// for a test session. They can be passed the same way `DD_PLAYWRIGHT_WORKER` is passed.
formattedSpan.meta[TEST_SESSION_ID] = this.testSessionSpan.context().toTraceId()
formattedSpan.meta[TEST_MODULE_ID] = this.testModuleSpan.context().toSpanId()
formattedSpan.meta[TEST_COMMAND] = this.command
formattedSpan.meta[TEST_MODULE] = this.constructor.id
// MISSING _trace.startTime and _trace.ticks - because by now the suite is already serialized
const testSuite = this._testSuites.get(formattedSpan.meta.test_suite_absolute_path)
if (testSuite) {
formattedSpan.meta[TEST_SUITE_ID] = testSuite.context().toSpanId()
}
// test_suite_absolute_path is just a hack because in the worker we don't have rootDir and repositoryRoot
// but if we pass those the same way we pass `DD_PLAYWRIGHT_WORKER` this is not necessary
const testSuitePath = getTestSuitePath(formattedSpan.meta.test_suite_absolute_path, this.rootDir)
const testSourceFile = getTestSuitePath(formattedSpan.meta.test_suite_absolute_path, this.repositoryRoot)
// we need to rewrite this because this.rootDir and this.repositoryRoot are not available in the worker
formattedSpan.meta[TEST_SUITE] = testSuitePath
formattedSpan.meta[TEST_SOURCE_FILE] = testSourceFile
formattedSpan.resource = `${testSuitePath}.${formattedSpan.meta[TEST_NAME]}`
delete formattedSpan.meta.test_suite_absolute_path
}
formattedTrace.push(formattedSpan)
}
formattedTraces.push(formattedTrace)
}
formattedTraces.forEach(trace => {
this.tracer._exporter.export(trace)
})
})
this.addSub('ci:playwright:test:finish', ({
span,
testStatus,
steps,
error,
extraTags,
isNew,
isEfdRetry,
isRetry,
isAttemptToFix,
isDisabled,
isQuarantined,
isAttemptToFixRetry,
hasFailedAllRetries,
hasPassedAttemptToFixRetries,
hasFailedAttemptToFixRetries,
isAtrRetry,
isModified,
onDone
}) => {
if (!span) return
const isRUMActive = span.context()._tags[TEST_IS_RUM_ACTIVE]
span.setTag(TEST_STATUS, testStatus)
if (error) {
span.setTag('error', error)
}
if (extraTags) {
span.addTags(extraTags)
}
if (isNew) {
span.setTag(TEST_IS_NEW, 'true')
if (isEfdRetry) {
span.setTag(TEST_IS_RETRY, 'true')
span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
}
}
if (isRetry) {
span.setTag(TEST_IS_RETRY, 'true')
if (isAtrRetry) {
span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr)
} else {
span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.ext)
}
}
if (hasFailedAllRetries) {
span.setTag(TEST_HAS_FAILED_ALL_RETRIES, 'true')
}
if (isAttemptToFix) {
span.setTag(TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX, 'true')
}
if (isAttemptToFixRetry) {
span.setTag(TEST_IS_RETRY, 'true')
span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atf)
}
if (hasPassedAttemptToFixRetries) {
span.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'true')
} else if (hasFailedAttemptToFixRetries) {
span.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'false')
}
if (isDisabled) {
span.setTag(TEST_MANAGEMENT_IS_DISABLED, 'true')
}
if (isQuarantined) {
span.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true')
}
if (isModified) {
span.setTag(TEST_IS_MODIFIED, 'true')
if (isEfdRetry) {
span.setTag(TEST_IS_RETRY, 'true')
span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
}
}
steps.forEach(step => {
const stepStartTime = step.startTime.getTime()
const stepSpan = this.tracer.startSpan('playwright.step', {
childOf: span,
startTime: stepStartTime,
tags: {
[COMPONENT]: this.constructor.id,
'playwright.step': step.title,
[RESOURCE_NAME]: step.title
}
})
if (step.error) {
stepSpan.setTag('error', step.error)
}
let stepDuration = step.duration
if (stepDuration <= 0 || Number.isNaN(stepDuration)) {
stepDuration = 0
}
stepSpan.finish(stepStartTime + stepDuration)
})
if (testStatus === 'fail') {
this.numFailedTests++
}
this.telemetry.ciVisEvent(
TELEMETRY_EVENT_FINISHED,
'test',
{
hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS],
isNew,
isRum: isRUMActive,
browserDriver: 'playwright'
}
)
span.finish()
finishAllTraceSpans(span)
if (getEnvironmentVariable('DD_PLAYWRIGHT_WORKER')) {
this.tracer._exporter.flush(onDone)
}
})
}
// TODO: this runs both in worker and main process (main process: skipped tests that do not go through _runTest)
startTestSpan (testName, testSuiteAbsolutePath, testSuite, testSourceFile, testSourceLine, browserName) {
const testSuiteSpan = this._testSuites.get(testSuiteAbsolutePath)
const extraTags = {
[TEST_SOURCE_START]: testSourceLine
}
if (testSourceFile) {
extraTags[TEST_SOURCE_FILE] = testSourceFile || testSuite
}
if (browserName) {
// Added as parameter too because it should affect the test fingerprint
extraTags[TEST_PARAMETERS] = JSON.stringify({ arguments: { browser: browserName }, metadata: {} })
extraTags[TEST_BROWSER_NAME] = browserName
}
extraTags.test_suite_absolute_path = testSuiteAbsolutePath
return super.startTestSpan(testName, testSuite, testSuiteSpan, extraTags)
}
}
module.exports = PlaywrightPlugin