diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b98e6db18a6..c07eedca3db 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -11,11 +11,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} cancel-in-progress: true -env: - SLACK_REPORT_ENABLE: ${{ github.event.schedule }} # value is empty for non-nightly jobs - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - SLACK_MOREINFO: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - # TODO: upstream jobs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cb3e51c56b4..77f516143c9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,6 +62,7 @@ deploy_to_reliability_env: deploy_to_docker_registries: stage: deploy + needs: [] rules: - if: '$CI_COMMIT_TAG =~ /^v.*/ || $CI_COMMIT_TAG == "dev"' when: on_success @@ -80,6 +81,7 @@ deploy_to_docker_registries: deploy_latest_to_docker_registries: stage: deploy + needs: [] rules: - if: '$CI_COMMIT_TAG =~ /^v.*/' when: on_success diff --git a/.npmignore b/.npmignore index af16b4ac73a..7b0a629739e 100644 --- a/.npmignore +++ b/.npmignore @@ -15,6 +15,7 @@ !esbuild.js !init.js !loader-hook.mjs +!register.js !package.json !cypress/**/* !ci/**/* diff --git a/README.md b/README.md index ecdf24e2abf..021604873f2 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,17 @@ but code loaded using `import` might not always work. Use the following command to enable experimental ESM support with your application: +Node.js < v20.6 + ```sh node --loader dd-trace/loader-hook.mjs entrypoint.js ``` +Node.js >= v20.6 + +```sh +node --import dd-trace/register.js entrypoint.js +``` ## Serverless / Lambda diff --git a/ci/cypress/after-spec.js b/ci/cypress/after-spec.js new file mode 100644 index 00000000000..9c3ae9da74d --- /dev/null +++ b/ci/cypress/after-spec.js @@ -0,0 +1 @@ +module.exports = require('../../packages/datadog-plugin-cypress/src/after-spec') diff --git a/docs/package.json b/docs/package.json index 3f58f83cbda..0a9097621fd 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "typedoc.js", "scripts": { - "build": "typedoc", + "build": "typedoc ../index.d.ts", "pretest": "tsc -p . && tsc test", "test": "node test" }, diff --git a/docs/test.ts b/docs/test.ts index 865de552afa..5f4cdbd801c 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -1,5 +1,6 @@ import { performance } from 'perf_hooks' import ddTrace, { tracer, Tracer, TracerOptions, Span, SpanContext, SpanOptions, Scope, User } from '..'; +import type { plugins } from '..'; import { opentelemetry } from '..'; import { formats, kinds, priority, tags, types } from '../ext'; import { BINARY, HTTP_HEADERS, LOG, TEXT_MAP } from '../ext/formats'; @@ -157,39 +158,39 @@ const httpOptions = { middleware: true }; -const httpServerOptions = { +const httpServerOptions: plugins.HttpServer = { ...httpOptions, hooks: { - request: (span: Span, req, res) => {} + request: (span?: Span, req?, res?) => {} } }; -const httpClientOptions = { +const httpClientOptions: plugins.HttpClient = { ...httpOptions, splitByDomain: true, propagationBlocklist: ['url', /url/, url => true], hooks: { - request: (span: Span, req, res) => {} + request: (span?: Span, req?, res?) => { } } }; -const http2ServerOptions = { +const http2ServerOptions: plugins.Http2Server = { ...httpOptions }; -const http2ClientOptions = { +const http2ClientOptions: plugins.Http2Client = { ...httpOptions, splitByDomain: true }; -const nextOptions = { +const nextOptions: plugins.next = { service: 'test', hooks: { - request: (span: Span, params) => { }, + request: (span?: Span, params?) => { }, }, }; -const graphqlOptions = { +const graphqlOptions: plugins.graphql = { service: 'test', depth: 2, source: true, @@ -197,24 +198,24 @@ const graphqlOptions = { collapse: false, signature: false, hooks: { - execute: (span: Span, args, res) => {}, - validate: (span: Span, document, errors) => {}, - parse: (span: Span, source, document) => {} + execute: (span?: Span, args?, res?) => {}, + validate: (span?: Span, document?, errors?) => {}, + parse: (span?: Span, source?, document?) => {} } }; -const elasticsearchOptions = { +const elasticsearchOptions: plugins.elasticsearch = { service: 'test', hooks: { - query: (span: Span, params) => {}, + query: (span?: Span, params?) => {}, }, }; -const awsSdkOptions = { +const awsSdkOptions: plugins.aws_sdk = { service: 'test', splitByAwsService: false, hooks: { - request: (span: Span, response) => {}, + request: (span?: Span, response?) => {}, }, s3: false, sqs: { @@ -223,33 +224,32 @@ const awsSdkOptions = { } }; -const redisOptions = { +const redisOptions: plugins.redis = { service: 'test', allowlist: ['info', /auth/i, command => true], blocklist: ['info', /auth/i, command => true], }; -const sharedbOptions = { +const sharedbOptions: plugins.sharedb = { service: 'test', hooks: { - receive: (span: Span, request) => {}, - reply: (span: Span, request, reply) => {}, + receive: (span?: Span, request?) => {}, + reply: (span?: Span, request?, reply?) => {}, }, }; -const moleculerOptions = { +const moleculerOptions: plugins.moleculer = { service: 'test', client: false, - params: true, server: { meta: true } }; -const openSearchOptions = { +const openSearchOptions: plugins.opensearch = { service: 'test', hooks: { - query: (span: Span, params) => {}, + query: (span?: Span, params?) => {}, }, }; @@ -356,8 +356,9 @@ tracer.use('express', { measured: true }); span = tracer.startSpan('test'); span = tracer.startSpan('test', {}); +span = tracer.startSpan('test', { childOf: span }); span = tracer.startSpan('test', { - childOf: span || span.context(), + childOf: span.context(), references: [], startTime: 123456789.1234, tags: { @@ -371,7 +372,7 @@ tracer.trace('test', { service: 'foo', resource: 'bar', type: 'baz' }, () => {}) tracer.trace('test', { measured: true }, () => {}) tracer.trace('test', (span: Span) => {}) tracer.trace('test', (span: Span, fn: () => void) => {}) -tracer.trace('test', (span: Span, fn: (err: Error) => string) => {}) +tracer.trace('test', (span: Span, fn: (err: Error) => void) => {}) promise = tracer.trace('test', () => Promise.resolve()) @@ -382,8 +383,9 @@ promise = tracer.wrap('test', () => Promise.resolve())() const carrier = {} -tracer.inject(span || span.context(), HTTP_HEADERS, carrier); -context = tracer.extract(HTTP_HEADERS, carrier); +tracer.inject(span, HTTP_HEADERS, carrier); +tracer.inject(span.context(), HTTP_HEADERS, carrier); +context = tracer.extract(HTTP_HEADERS, carrier)!; traceId = context.toTraceId(); spanId = context.toSpanId(); @@ -391,7 +393,7 @@ traceparent = context.toTraceparent(); const scope = tracer.scope() -span = scope.active(); +span = scope.active()!; const activateStringType: string = scope.activate(span, () => 'test'); const activateVoidType: void = scope.activate(span, () => {}); diff --git a/docs/typedoc.js b/docs/typedoc.js index bce72b793e2..ccb5c55c04c 100644 --- a/docs/typedoc.js +++ b/docs/typedoc.js @@ -4,8 +4,8 @@ module.exports = { excludeExternals: true, excludePrivate: true, excludeProtected: true, - includeDeclarations: true, - mode: 'file', + // includeDeclarations: true, + // mode: 'file', name: 'dd-trace', out: 'out', readme: 'API.md' diff --git a/docs/yarn.lock b/docs/yarn.lock index 08fe81056b4..0e133fea97f 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -69,9 +69,9 @@ handlebars@^4.7.6: uglify-js "^3.1.4" hasown@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" - integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + version "2.0.1" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa" + integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA== dependencies: function-bind "^1.1.2" diff --git a/index.d.ts b/index.d.ts index eb298f1fbc8..4045995d4d9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,22 +1,29 @@ import { ClientRequest, IncomingMessage, OutgoingMessage, ServerResponse } from "http"; import { LookupFunction } from 'net'; import * as opentracing from "opentracing"; -import { SpanOptions } from "opentracing/lib/tracer"; import * as otel from "@opentelemetry/api"; -export { SpanOptions }; - /** * Tracer is the entry-point of the Datadog tracing implementation. */ -export declare interface Tracer extends opentracing.Tracer { +interface Tracer extends opentracing.Tracer { + /** + * Add tracer as a named export + */ + tracer: Tracer; + + /** + * For compatibility with NodeNext + esModuleInterop: false + */ + default: Tracer; + /** * Starts and returns a new Span representing a logical unit of work. * @param {string} name The name of the operation. - * @param {SpanOptions} [options] Options for the newly created span. + * @param {tracer.SpanOptions} [options] Options for the newly created span. * @returns {Span} A new Span object. */ - startSpan (name: string, options?: SpanOptions): Span; + startSpan (name: string, options?: tracer.SpanOptions): tracer.Span; /** * Injects the given SpanContext instance for cross-process propagation @@ -28,7 +35,7 @@ export declare interface Tracer extends opentracing.Tracer { * @param {string} format The format of the carrier. * @param {any} carrier The carrier object. */ - inject (spanContext: SpanContext | Span, format: string, carrier: any): void; + inject (spanContext: tracer.SpanContext | tracer.Span, format: string, carrier: any): void; /** * Returns a SpanContext instance extracted from `carrier` in the given @@ -39,12 +46,12 @@ export declare interface Tracer extends opentracing.Tracer { * The extracted SpanContext, or null if no such SpanContext could * be found in `carrier` */ - extract (format: string, carrier: any): SpanContext | null; + extract (format: string, carrier: any): tracer.SpanContext | null; /** * Initializes the tracer. This should be called before importing other libraries. */ - init (options?: TracerOptions): this; + init (options?: tracer.TracerOptions): this; /** * Sets the URL for the trace agent. This should only be called _after_ @@ -63,7 +70,7 @@ export declare interface Tracer extends opentracing.Tracer { /** * Returns a reference to the current scope. */ - scope (): Scope; + scope (): tracer.Scope; /** * Instruments a function by automatically creating a span activated on its @@ -83,8 +90,9 @@ export declare interface Tracer extends opentracing.Tracer { * unless there is already an active span or `childOf` option. Note that this * option is deprecated and has been removed in version 4.0. */ - trace (name: string, fn: (span?: Span, fn?: (error?: Error) => any) => T): T; - trace (name: string, options: TraceOptions & SpanOptions, fn: (span?: Span, done?: (error?: Error) => string) => T): T; + trace (name: string, fn: (span: tracer.Span) => T): T; + trace (name: string, fn: (span: tracer.Span, done: (error?: Error) => void) => T): T; + trace (name: string, options: tracer.TraceOptions & tracer.SpanOptions, fn: (span?: tracer.Span, done?: (error?: Error) => void) => T): T; /** * Wrap a function to automatically create a span activated on its @@ -101,8 +109,8 @@ export declare interface Tracer extends opentracing.Tracer { * which case the span will finish at the end of the function execution. */ wrap any> (name: string, fn: T): T; - wrap any> (name: string, options: TraceOptions & SpanOptions, fn: T): T; - wrap any> (name: string, options: (...args: any[]) => TraceOptions & SpanOptions, fn: T): T; + wrap any> (name: string, options: tracer.TraceOptions & tracer.SpanOptions, fn: T): T; + wrap any> (name: string, options: (...args: any[]) => tracer.TraceOptions & tracer.SpanOptions, fn: T): T; /** * Create and return a string that can be included in the of a @@ -116,1877 +124,1882 @@ export declare interface Tracer extends opentracing.Tracer { * @param {User} user Properties of the authenticated user. Accepts custom fields. * @returns {Tracer} The Tracer instance for chaining. */ - setUser (user: User): Tracer; + setUser (user: tracer.User): Tracer; - appsec: Appsec; + appsec: tracer.Appsec; - TracerProvider: opentelemetry.TracerProvider; + TracerProvider: tracer.opentelemetry.TracerProvider; - dogstatsd: DogStatsD; + dogstatsd: tracer.DogStatsD; } -export declare interface TraceOptions extends Analyzable { - /** - * The resource you are tracing. The resource name must not be longer than - * 5000 characters. - */ - resource?: string, - - /** - * The service you are tracing. The service name must not be longer than - * 100 characters. - */ - service?: string, - - /** - * The type of request. - */ - type?: string - - /** - * An array of span links - */ - links?: Array<{ context: SpanContext, attributes?: Object }> +// left out of the namespace, so it +// is doesn't need to be exported for Tracer +/** @hidden */ +interface Plugins { + "amqp10": tracer.plugins.amqp10; + "amqplib": tracer.plugins.amqplib; + "aws-sdk": tracer.plugins.aws_sdk; + "bunyan": tracer.plugins.bunyan; + "cassandra-driver": tracer.plugins.cassandra_driver; + "connect": tracer.plugins.connect; + "couchbase": tracer.plugins.couchbase; + "cucumber": tracer.plugins.cucumber; + "cypress": tracer.plugins.cypress; + "dns": tracer.plugins.dns; + "elasticsearch": tracer.plugins.elasticsearch; + "express": tracer.plugins.express; + "fastify": tracer.plugins.fastify; + "fetch": tracer.plugins.fetch; + "generic-pool": tracer.plugins.generic_pool; + "google-cloud-pubsub": tracer.plugins.google_cloud_pubsub; + "graphql": tracer.plugins.graphql; + "grpc": tracer.plugins.grpc; + "hapi": tracer.plugins.hapi; + "http": tracer.plugins.http; + "http2": tracer.plugins.http2; + "ioredis": tracer.plugins.ioredis; + "jest": tracer.plugins.jest; + "kafkajs": tracer.plugins.kafkajs + "knex": tracer.plugins.knex; + "koa": tracer.plugins.koa; + "mariadb": tracer.plugins.mariadb; + "memcached": tracer.plugins.memcached; + "microgateway-core": tracer.plugins.microgateway_core; + "mocha": tracer.plugins.mocha; + "moleculer": tracer.plugins.moleculer; + "mongodb-core": tracer.plugins.mongodb_core; + "mongoose": tracer.plugins.mongoose; + "mysql": tracer.plugins.mysql; + "mysql2": tracer.plugins.mysql2; + "net": tracer.plugins.net; + "next": tracer.plugins.next; + "openai": tracer.plugins.openai; + "opensearch": tracer.plugins.opensearch; + "oracledb": tracer.plugins.oracledb; + "paperplane": tracer.plugins.paperplane; + "playwright": tracer.plugins.playwright; + "pg": tracer.plugins.pg; + "pino": tracer.plugins.pino; + "redis": tracer.plugins.redis; + "restify": tracer.plugins.restify; + "rhea": tracer.plugins.rhea; + "router": tracer.plugins.router; + "sharedb": tracer.plugins.sharedb; + "tedious": tracer.plugins.tedious; + "winston": tracer.plugins.winston; } -/** - * Span represents a logical unit of work as part of a broader Trace. - * Examples of span might include remote procedure calls or a in-process - * function calls to sub-components. A Trace has a single, top-level "root" - * Span that in turn may have zero or more child Spans, which in turn may - * have children. - */ -export declare interface Span extends opentracing.Span { - context (): SpanContext; - - /** - * Causally links another span to the current span - * @param {SpanContext} context The context of the span to link to. - * @param {Object} attributes An optional key value pair of arbitrary values. - * @returns {void} - */ - addLink (context: SpanContext, attributes?: Object): void; -} +declare namespace tracer { + export type SpanOptions = opentracing.SpanOptions; + export { Tracer }; -/** - * SpanContext represents Span state that must propagate to descendant Spans - * and across process boundaries. - * - * SpanContext is logically divided into two pieces: the user-level "Baggage" - * (see setBaggageItem and getBaggageItem) that propagates across Span - * boundaries and any Tracer-implementation-specific fields that are needed to - * identify or otherwise contextualize the associated Span instance (e.g., a - * tuple). - */ -export declare interface SpanContext extends opentracing.SpanContext { - /** - * Returns the string representation of the internal trace ID. - */ - toTraceId (): string; + export interface TraceOptions extends Analyzable { + /** + * The resource you are tracing. The resource name must not be longer than + * 5000 characters. + */ + resource?: string, - /** - * Returns the string representation of the internal span ID. - */ - toSpanId (): string; + /** + * The service you are tracing. The service name must not be longer than + * 100 characters. + */ + service?: string, - /** - * Returns the string representation used for DBM integration. - */ - toTraceparent (): string; -} + /** + * The type of request. + */ + type?: string -/** - * Sampling rule to configure on the priority sampler. - */ -export declare interface SamplingRule { - /** - * Sampling rate for this rule. - */ - sampleRate: number + /** + * An array of span links + */ + links?: Array<{ context: SpanContext, attributes?: Object }> + } /** - * Service on which to apply this rule. The rule will apply to all services if not provided. + * Span represents a logical unit of work as part of a broader Trace. + * Examples of span might include remote procedure calls or a in-process + * function calls to sub-components. A Trace has a single, top-level "root" + * Span that in turn may have zero or more child Spans, which in turn may + * have children. */ - service?: string | RegExp + export interface Span extends opentracing.Span { + context (): SpanContext; - /** - * Operation name on which to apply this rule. The rule will apply to all operation names if not provided. - */ - name?: string | RegExp -} + /** + * Causally links another span to the current span + * @param {SpanContext} context The context of the span to link to. + * @param {Object} attributes An optional key value pair of arbitrary values. + * @returns {void} + */ + addLink (context: SpanContext, attributes?: Object): void; + } -/** - * Span sampling rules to ingest single spans where the enclosing trace is dropped - */ -export declare interface SpanSamplingRule { /** - * Sampling rate for this rule. Will default to 1.0 (always) if not provided. + * SpanContext represents Span state that must propagate to descendant Spans + * and across process boundaries. + * + * SpanContext is logically divided into two pieces: the user-level "Baggage" + * (see setBaggageItem and getBaggageItem) that propagates across Span + * boundaries and any Tracer-implementation-specific fields that are needed to + * identify or otherwise contextualize the associated Span instance (e.g., a + * tuple). */ - sampleRate?: number + export interface SpanContext extends opentracing.SpanContext { + /** + * Returns the string representation of the internal trace ID. + */ + toTraceId (): string; - /** - * Maximum number of spans matching a span sampling rule to be allowed per second. - */ - maxPerSecond?: number + /** + * Returns the string representation of the internal span ID. + */ + toSpanId (): string; - /** - * Service name or pattern on which to apply this rule. The rule will apply to all services if not provided. - */ - service?: string + /** + * Returns the string representation used for DBM integration. + */ + toTraceparent (): string; + } /** - * Operation name or pattern on which to apply this rule. The rule will apply to all operation names if not provided. + * Sampling rule to configure on the priority sampler. */ - name?: string -} + export interface SamplingRule { + /** + * Sampling rate for this rule. + */ + sampleRate: number -/** - * Selection and priority order of context propagation injection and extraction mechanisms. - */ -export declare interface PropagationStyle { - /** - * Selection of context propagation injection mechanisms. - */ - inject: string[], + /** + * Service on which to apply this rule. The rule will apply to all services if not provided. + */ + service?: string | RegExp - /** - * Selection and priority order of context propagation extraction mechanisms. - */ - extract: string[] -} + /** + * Operation name on which to apply this rule. The rule will apply to all operation names if not provided. + */ + name?: string | RegExp + } -/** - * List of options available to the tracer. - */ -export declare interface TracerOptions { /** - * Whether to enable trace ID injection in log records to be able to correlate - * traces with logs. - * @default false + * Span sampling rules to ingest single spans where the enclosing trace is dropped */ - logInjection?: boolean, + export interface SpanSamplingRule { + /** + * Sampling rate for this rule. Will default to 1.0 (always) if not provided. + */ + sampleRate?: number - /** - * Whether to enable startup logs. - * @default true - */ - startupLogs?: boolean, + /** + * Maximum number of spans matching a span sampling rule to be allowed per second. + */ + maxPerSecond?: number - /** - * The service name to be used for this program. If not set, the service name - * will attempted to be inferred from package.json - */ - service?: string; + /** + * Service name or pattern on which to apply this rule. The rule will apply to all services if not provided. + */ + service?: string - /** - * Provide service name mappings for each plugin. - */ - serviceMapping?: { [key: string]: string }; + /** + * Operation name or pattern on which to apply this rule. The rule will apply to all operation names if not provided. + */ + name?: string + } /** - * The url of the trace agent that the tracer will submit to. - * Takes priority over hostname and port, if set. + * Selection and priority order of context propagation injection and extraction mechanisms. */ - url?: string; + export interface PropagationStyle { + /** + * Selection of context propagation injection mechanisms. + */ + inject: string[], - /** - * The address of the trace agent that the tracer will submit to. - * @default 'localhost' - */ - hostname?: string; + /** + * Selection and priority order of context propagation extraction mechanisms. + */ + extract: string[] + } /** - * The port of the trace agent that the tracer will submit to. - * @default 8126 + * List of options available to the tracer. */ - port?: number | string; + export interface TracerOptions { + /** + * Whether to enable trace ID injection in log records to be able to correlate + * traces with logs. + * @default false + */ + logInjection?: boolean, - /** - * Whether to enable profiling. - */ - profiling?: boolean + /** + * Whether to enable startup logs. + * @default true + */ + startupLogs?: boolean, - /** - * Options specific for the Dogstatsd agent. - */ - dogstatsd?: { /** - * The hostname of the Dogstatsd agent that the metrics will submitted to. + * The service name to be used for this program. If not set, the service name + * will attempted to be inferred from package.json */ - hostname?: string + service?: string; /** - * The port of the Dogstatsd agent that the metrics will submitted to. - * @default 8125 + * Provide service name mappings for each plugin. */ - port?: number - }; + serviceMapping?: { [key: string]: string }; - /** - * Set an application’s environment e.g. prod, pre-prod, stage. - */ - env?: string; + /** + * The url of the trace agent that the tracer will submit to. + * Takes priority over hostname and port, if set. + */ + url?: string; - /** - * The version number of the application. If not set, the version - * will attempted to be inferred from package.json. - */ - version?: string; + /** + * The address of the trace agent that the tracer will submit to. + * @default 'localhost' + */ + hostname?: string; - /** - * Controls the ingestion sample rate (between 0 and 1) between the agent and the backend. - */ - sampleRate?: number; + /** + * The port of the trace agent that the tracer will submit to. + * @default 8126 + */ + port?: number | string; - /** - * Global rate limit that is applied on the global sample rate and all rules, - * and controls the ingestion rate limit between the agent and the backend. - * Defaults to deferring the decision to the agent. - */ - rateLimit?: number, + /** + * Whether to enable profiling. + */ + profiling?: boolean - /** - * Sampling rules to apply to priority samplin. Each rule is a JSON, - * consisting of `service` and `name`, which are regexes to match against - * a trace's `service` and `name`, and a corresponding `sampleRate`. If not - * specified, will defer to global sampling rate for all spans. - * @default [] - */ - samplingRules?: SamplingRule[] + /** + * Options specific for the Dogstatsd agent. + */ + dogstatsd?: { + /** + * The hostname of the Dogstatsd agent that the metrics will submitted to. + */ + hostname?: string - /** - * Span sampling rules that take effect when the enclosing trace is dropped, to ingest single spans - * @default [] - */ - spanSamplingRules?: SpanSamplingRule[] + /** + * The port of the Dogstatsd agent that the metrics will submitted to. + * @default 8125 + */ + port?: number + }; - /** - * Interval in milliseconds at which the tracer will submit traces to the agent. - * @default 2000 - */ - flushInterval?: number; + /** + * Set an application’s environment e.g. prod, pre-prod, stage. + */ + env?: string; - /** - * Number of spans before partially exporting a trace. This prevents keeping all the spans in memory for very large traces. - * @default 1000 - */ - flushMinSpans?: number; + /** + * The version number of the application. If not set, the version + * will attempted to be inferred from package.json. + */ + version?: string; - /** - * Whether to enable runtime metrics. - * @default false - */ - runtimeMetrics?: boolean + /** + * Controls the ingestion sample rate (between 0 and 1) between the agent and the backend. + */ + sampleRate?: number; - /** - * Custom function for DNS lookups when sending requests to the agent. - * @default dns.lookup() - */ - lookup?: LookupFunction + /** + * Global rate limit that is applied on the global sample rate and all rules, + * and controls the ingestion rate limit between the agent and the backend. + * Defaults to deferring the decision to the agent. + */ + rateLimit?: number, - /** - * Protocol version to use for requests to the agent. The version configured must be supported by the agent version installed or all traces will be dropped. - * @default 0.4 - */ - protocolVersion?: string + /** + * Sampling rules to apply to priority samplin. Each rule is a JSON, + * consisting of `service` and `name`, which are regexes to match against + * a trace's `service` and `name`, and a corresponding `sampleRate`. If not + * specified, will defer to global sampling rate for all spans. + * @default [] + */ + samplingRules?: SamplingRule[] - /** - * Deprecated in favor of the global versions of the variables provided under this option - * - * @deprecated - * @hidden - */ - ingestion?: { /** - * Controls the ingestion sample rate (between 0 and 1) between the agent and the backend. + * Span sampling rules that take effect when the enclosing trace is dropped, to ingest single spans + * @default [] */ - sampleRate?: number + spanSamplingRules?: SpanSamplingRule[] /** - * Controls the ingestion rate limit between the agent and the backend. Defaults to deferring the decision to the agent. + * Interval in milliseconds at which the tracer will submit traces to the agent. + * @default 2000 */ - rateLimit?: number - }; + flushInterval?: number; - /** - * Experimental features can be enabled individually using key / value pairs. - * @default {} - */ - experimental?: { - b3?: boolean - traceparent?: boolean + /** + * Number of spans before partially exporting a trace. This prevents keeping all the spans in memory for very large traces. + * @default 1000 + */ + flushMinSpans?: number; /** - * Whether to add an auto-generated `runtime-id` tag to metrics. + * Whether to enable runtime metrics. * @default false */ - runtimeId?: boolean + runtimeMetrics?: boolean /** - * Whether to write traces to log output or agentless, rather than send to an agent - * @default false + * Custom function for DNS lookups when sending requests to the agent. + * @default dns.lookup() */ - exporter?: 'log' | 'agent' | 'datadog' + lookup?: LookupFunction /** - * Whether to enable the experimental `getRumData` method. - * @default false + * Protocol version to use for requests to the agent. The version configured must be supported by the agent version installed or all traces will be dropped. + * @default 0.4 */ - enableGetRumData?: boolean + protocolVersion?: string /** - * Configuration of the IAST. Can be a boolean as an alias to `iast.enabled`. + * Deprecated in favor of the global versions of the variables provided under this option + * + * @deprecated + * @hidden */ - iast?: boolean | { - /** - * Whether to enable IAST. - * @default false - */ - enabled?: boolean, - /** - * Controls the percentage of requests that iast will analyze - * @default 30 - */ - requestSampling?: number, + ingestion?: { /** - * Controls how many request can be analyzing code vulnerabilities at the same time - * @default 2 + * Controls the ingestion sample rate (between 0 and 1) between the agent and the backend. */ - maxConcurrentRequests?: number, + sampleRate?: number + /** - * Controls how many code vulnerabilities can be detected in the same request - * @default 2 + * Controls the ingestion rate limit between the agent and the backend. Defaults to deferring the decision to the agent. */ - maxContextOperations?: number, + rateLimit?: number + }; + + /** + * Experimental features can be enabled individually using key / value pairs. + * @default {} + */ + experimental?: { + b3?: boolean + traceparent?: boolean + /** - * Whether to enable vulnerability deduplication + * Whether to add an auto-generated `runtime-id` tag to metrics. + * @default false */ - deduplicationEnabled?: boolean + runtimeId?: boolean + /** - * Whether to enable vulnerability redaction - * @default true + * Whether to write traces to log output or agentless, rather than send to an agent + * @default false */ - redactionEnabled?: boolean, + exporter?: 'log' | 'agent' | 'datadog' + /** - * Specifies a regex that will redact sensitive source names in vulnerability reports. + * Whether to enable the experimental `getRumData` method. + * @default false */ - redactionNamePattern?: string, + enableGetRumData?: boolean + /** - * Specifies a regex that will redact sensitive source values in vulnerability reports. + * Configuration of the IAST. Can be a boolean as an alias to `iast.enabled`. */ - redactionValuePattern?: string - } - }; - - /** - * Whether to load all built-in plugins. - * @default true - */ - plugins?: boolean; - - /** - * Custom logger to be used by the tracer (if debug = true), - * should support error(), warn(), info(), and debug() methods - * see https://datadog.github.io/dd-trace-js/#custom-logging - */ - logger?: { - error: (err: Error | string) => void; - warn: (message: string) => void; - info: (message: string) => void; - debug: (message: string) => void; - }; - - /** - * Global tags that should be assigned to every span. - */ - tags?: { [key: string]: any }; - - /** - * Specifies which scope implementation to use. The default is to use the best - * implementation for the runtime. Only change this if you know what you are - * doing. - */ - scope?: 'async_hooks' | 'async_local_storage' | 'async_resource' | 'sync' | 'noop' - - /** - * Whether to report the hostname of the service host. This is used when the agent is deployed on a different host and cannot determine the hostname automatically. - * @default false - */ - reportHostname?: boolean - - /** - * A string representing the minimum tracer log level to use when debug logging is enabled - * @default 'debug' - */ - logLevel?: 'error' | 'debug' - - /** - * If false, require a parent in order to trace. - * @default true - * @deprecated since version 4.0 - */ - orphanable?: boolean - - /** - * Enables DBM to APM link using tag injection. - * @default 'disabled' - */ - dbmPropagationMode?: 'disabled' | 'service' | 'full' - - /** - * Configuration of the AppSec protection. Can be a boolean as an alias to `appsec.enabled`. - */ - appsec?: boolean | { - /** - * Whether to enable AppSec. - * @default false - */ - enabled?: boolean, + iast?: boolean | { + /** + * Whether to enable IAST. + * @default false + */ + enabled?: boolean, + /** + * Controls the percentage of requests that iast will analyze + * @default 30 + */ + requestSampling?: number, + /** + * Controls how many request can be analyzing code vulnerabilities at the same time + * @default 2 + */ + maxConcurrentRequests?: number, + /** + * Controls how many code vulnerabilities can be detected in the same request + * @default 2 + */ + maxContextOperations?: number, + /** + * Whether to enable vulnerability deduplication + */ + deduplicationEnabled?: boolean, + /** + * Whether to enable vulnerability redaction + * @default true + */ + redactionEnabled?: boolean, + /** + * Specifies a regex that will redact sensitive source names in vulnerability reports. + */ + redactionNamePattern?: string, + /** + * Specifies a regex that will redact sensitive source values in vulnerability reports. + */ + redactionValuePattern?: string + } + }; /** - * Specifies a path to a custom rules file. + * Whether to load all built-in plugins. + * @default true */ - rules?: string, + plugins?: boolean; /** - * Controls the maximum amount of traces sampled by AppSec attacks, per second. - * @default 100 + * Custom logger to be used by the tracer (if debug = true), + * should support error(), warn(), info(), and debug() methods + * see https://datadog.github.io/dd-trace-js/#custom-logging */ - rateLimit?: number, + logger?: { + error: (err: Error | string) => void; + warn: (message: string) => void; + info: (message: string) => void; + debug: (message: string) => void; + }; /** - * Controls the maximum amount of time in microseconds the WAF is allowed to run synchronously for. - * @default 5000 + * Global tags that should be assigned to every span. */ - wafTimeout?: number, + tags?: { [key: string]: any }; /** - * Specifies a regex that will redact sensitive data by its key in attack reports. + * Specifies which scope implementation to use. The default is to use the best + * implementation for the runtime. Only change this if you know what you are + * doing. */ - obfuscatorKeyRegex?: string, + scope?: 'async_hooks' | 'async_local_storage' | 'async_resource' | 'sync' | 'noop' /** - * Specifies a regex that will redact sensitive data by its value in attack reports. + * Whether to report the hostname of the service host. This is used when the agent is deployed on a different host and cannot determine the hostname automatically. + * @default false */ - obfuscatorValueRegex?: string, + reportHostname?: boolean /** - * Specifies a path to a custom blocking template html file. + * A string representing the minimum tracer log level to use when debug logging is enabled + * @default 'debug' */ - blockedTemplateHtml?: string, + logLevel?: 'error' | 'debug' /** - * Specifies a path to a custom blocking template json file. + * If false, require a parent in order to trace. + * @default true + * @deprecated since version 4.0 */ - blockedTemplateJson?: string, + orphanable?: boolean /** - * Specifies a path to a custom blocking template json file for graphql requests + * Enables DBM to APM link using tag injection. + * @default 'disabled' */ - blockedTemplateGraphql?: string, + dbmPropagationMode?: 'disabled' | 'service' | 'full' /** - * Controls the automated user event tracking configuration + * Configuration of the AppSec protection. Can be a boolean as an alias to `appsec.enabled`. */ - eventTracking?: { + appsec?: boolean | { /** - * Controls the automated user event tracking mode. Possible values are disabled, safe and extended. - * On safe mode, any detected Personally Identifiable Information (PII) about the user will be redacted from the event. - * On extended mode, no redaction will take place. - * @default 'safe' - */ - mode?: 'safe' | 'extended' | 'disabled' - }, - - /** - * Configuration for Api Security sampling - */ - apiSecurity?: { - /** Whether to enable Api Security. + * Whether to enable AppSec. * @default false */ enabled?: boolean, - /** Controls the request sampling rate (between 0 and 1) in which Api Security is triggered. - * The value will be coerced back if it's outside of the 0-1 range. - * @default 0.1 + /** + * Specifies a path to a custom rules file. */ - requestSampling?: number - } - }; + rules?: string, - /** - * Configuration of ASM Remote Configuration - */ - remoteConfig?: { - /** - * Specifies the remote configuration polling interval in seconds - * @default 5 - */ - pollInterval?: number, - } - - /** - * Whether to enable client IP collection from relevant IP headers - * @default false - */ - clientIpEnabled?: boolean - - /** - * Custom header name to source the http.client_ip tag from. - */ - clientIpHeader?: string, - - /** - * The selection and priority order of context propagation injection and extraction mechanisms. - */ - propagationStyle?: string[] | PropagationStyle -} - -/** - * User object that can be passed to `tracer.setUser()`. - */ -export declare interface User { - /** - * Unique identifier of the user. - * Mandatory. - */ - id: string, - - /** - * Email of the user. - */ - email?: string, - - /** - * User-friendly name of the user. - */ - name?: string, - - /** - * Session ID of the user. - */ - session_id?: string, - - /** - * Role the user is making the request under. - */ - role?: string, - - /** - * Scopes or granted authorizations the user currently possesses. - * The value could come from the scope associated with an OAuth2 - * Access Token or an attribute value in a SAML 2 Assertion. - */ - scope?: string, - - /** - * Custom fields to attach to the user (RBAC, Oauth, etc…). - */ - [key: string]: string | undefined -} - -export declare interface DogStatsD { - /** - * Increments a metric by the specified value, optionally specifying tags. - * @param {string} stat The dot-separated metric name. - * @param {number} value The amount to increment the stat by. - * @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags. - */ - increment(stat: string, value?: number, tags?: { [tag: string]: string|number }): void - - /** - * Decrements a metric by the specified value, optionally specifying tags. - * @param {string} stat The dot-separated metric name. - * @param {number} value The amount to decrement the stat by. - * @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags. - */ - decrement(stat: string, value?: number, tags?: { [tag: string]: string|number }): void - - /** - * Sets a distribution value, optionally specifying tags. - * @param {string} stat The dot-separated metric name. - * @param {number} value The amount to increment the stat by. - * @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags. - */ - distribution(stat: string, value?: number, tags?: { [tag: string]: string|number }): void - - /** - * Sets a gauge value, optionally specifying tags. - * @param {string} stat The dot-separated metric name. - * @param {number} value The amount to increment the stat by. - * @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags. - */ - gauge(stat: string, value?: number, tags?: { [tag: string]: string|number }): void - - /** - * Forces any unsent metrics to be sent - * - * @beta This method is experimental and could be removed in future versions. - */ - flush(): void -} - -export declare interface Appsec { - /** - * Links a successful login event to the current trace. Will link the passed user to the current trace with Appsec.setUser() internally. - * @param {User} user Properties of the authenticated user. Accepts custom fields. - * @param {[key: string]: string} metadata Custom fields to link to the login success event. - * - * @beta This method is in beta and could change in future versions. - */ - trackUserLoginSuccessEvent(user: User, metadata?: { [key: string]: string }): void + /** + * Controls the maximum amount of traces sampled by AppSec attacks, per second. + * @default 100 + */ + rateLimit?: number, - /** - * Links a failed login event to the current trace. - * @param {string} userId The user id of the attemped login. - * @param {boolean} exists If the user id exists. - * @param {[key: string]: string} metadata Custom fields to link to the login failure event. - * - * @beta This method is in beta and could change in future versions. - */ - trackUserLoginFailureEvent(userId: string, exists: boolean, metadata?: { [key: string]: string }): void + /** + * Controls the maximum amount of time in microseconds the WAF is allowed to run synchronously for. + * @default 5000 + */ + wafTimeout?: number, - /** - * Links a custom event to the current trace. - * @param {string} eventName The name of the event. - * @param {[key: string]: string} metadata Custom fields to link to the event. - * - * @beta This method is in beta and could change in future versions. - */ - trackCustomEvent(eventName: string, metadata?: { [key: string]: string }): void + /** + * Specifies a regex that will redact sensitive data by its key in attack reports. + */ + obfuscatorKeyRegex?: string, - /** - * Checks if the passed user should be blocked according to AppSec rules. - * If no user is linked to the current trace, will link the passed user to it. - * @param {User} user Properties of the authenticated user. Accepts custom fields. - * @return {boolean} Indicates whether the user should be blocked. - * - * @beta This method is in beta and could change in the future - */ - isUserBlocked(user: User): boolean + /** + * Specifies a regex that will redact sensitive data by its value in attack reports. + */ + obfuscatorValueRegex?: string, - /** - * Sends a "blocked" template response based on the request accept header and ends the response. - * **You should stop processing the request after calling this function!** - * @param {IncomingMessage} req Can be passed to force which request to act on. Optional. - * @param {OutgoingMessage} res Can be passed to force which response to act on. Optional. - * @return {boolean} Indicates if the action was successful. - * - * @beta This method is in beta and could change in the future - */ - blockRequest(req?: IncomingMessage, res?: OutgoingMessage): boolean + /** + * Specifies a path to a custom blocking template html file. + */ + blockedTemplateHtml?: string, - /** - * Links an authenticated user to the current trace. - * @param {User} user Properties of the authenticated user. Accepts custom fields. - * - * @beta This method is in beta and could change in the future - */ - setUser(user: User): void -} + /** + * Specifies a path to a custom blocking template json file. + */ + blockedTemplateJson?: string, -/** @hidden */ -declare type anyObject = { - [key: string]: any; -}; + /** + * Specifies a path to a custom blocking template json file for graphql requests + */ + blockedTemplateGraphql?: string, -/** @hidden */ -interface TransportRequestParams { - method: string; - path: string; - body?: anyObject; - bulkBody?: anyObject; - querystring?: anyObject; -} + /** + * Controls the automated user event tracking configuration + */ + eventTracking?: { + /** + * Controls the automated user event tracking mode. Possible values are disabled, safe and extended. + * On safe mode, any detected Personally Identifiable Information (PII) about the user will be redacted from the event. + * On extended mode, no redaction will take place. + * @default 'safe' + */ + mode?: 'safe' | 'extended' | 'disabled' + }, + /** + * Configuration for Api Security sampling + */ + apiSecurity?: { + /** Whether to enable Api Security. + * @default false + */ + enabled?: boolean, + + /** Controls the request sampling rate (between 0 and 1) in which Api Security is triggered. + * The value will be coerced back if it's outside of the 0-1 range. + * @default 0.1 + */ + requestSampling?: number + } + }; -/** - * The Datadog Scope Manager. This is used for context propagation. - */ -export declare interface Scope { - /** - * Get the current active span or null if there is none. - * - * @returns {Span} The active span. - */ - active (): Span | null; + /** + * Configuration of ASM Remote Configuration + */ + remoteConfig?: { + /** + * Specifies the remote configuration polling interval in seconds + * @default 5 + */ + pollInterval?: number, + } - /** - * Activate a span in the scope of a function. - * - * @param {Span} span The span to activate. - * @param {Function} fn Function that will have the span activated on its scope. - * @returns The return value of the provided function. - */ - activate (span: Span, fn: ((...args: any[]) => T)): T; + /** + * Whether to enable client IP collection from relevant IP headers + * @default false + */ + clientIpEnabled?: boolean - /** - * Binds a target to the provided span, or the active span if omitted. - * - * @param {Function|Promise} target Target that will have the span activated on its scope. - * @param {Span} [span=scope.active()] The span to activate. - * @returns The bound target. - */ - bind void> (fn: T, span?: Span | null): T; - bind V> (fn: T, span?: Span | null): T; - bind (fn: Promise, span?: Span | null): Promise; -} + /** + * Custom header name to source the http.client_ip tag from. + */ + clientIpHeader?: string, -/** @hidden */ -interface Plugins { - "amqp10": plugins.amqp10; - "amqplib": plugins.amqplib; - "aws-sdk": plugins.aws_sdk; - "bunyan": plugins.bunyan; - "cassandra-driver": plugins.cassandra_driver; - "connect": plugins.connect; - "couchbase": plugins.couchbase; - "cucumber": plugins.cucumber; - "cypress": plugins.cypress; - "dns": plugins.dns; - "elasticsearch": plugins.elasticsearch; - "express": plugins.express; - "fastify": plugins.fastify; - "fetch": plugins.fetch; - "generic-pool": plugins.generic_pool; - "google-cloud-pubsub": plugins.google_cloud_pubsub; - "graphql": plugins.graphql; - "grpc": plugins.grpc; - "hapi": plugins.hapi; - "http": plugins.http; - "http2": plugins.http2; - "ioredis": plugins.ioredis; - "jest": plugins.jest; - "kafkajs": plugins.kafkajs - "knex": plugins.knex; - "koa": plugins.koa; - "mariadb": plugins.mariadb; - "memcached": plugins.memcached; - "microgateway-core": plugins.microgateway_core; - "mocha": plugins.mocha; - "moleculer": plugins.moleculer; - "mongodb-core": plugins.mongodb_core; - "mongoose": plugins.mongoose; - "mysql": plugins.mysql; - "mysql2": plugins.mysql2; - "net": plugins.net; - "next": plugins.next; - "openai": plugins.openai; - "opensearch": plugins.opensearch; - "oracledb": plugins.oracledb; - "paperplane": plugins.paperplane; - "playwright": plugins.playwright; - "pg": plugins.pg; - "pino": plugins.pino; - "redis": plugins.redis; - "restify": plugins.restify; - "rhea": plugins.rhea; - "router": plugins.router; - "sharedb": plugins.sharedb; - "tedious": plugins.tedious; - "winston": plugins.winston; -} + /** + * The selection and priority order of context propagation injection and extraction mechanisms. + */ + propagationStyle?: string[] | PropagationStyle + } -/** @hidden */ -interface Analyzable { /** - * Whether to measure the span. Can also be set to a key-value pair with span - * names as keys and booleans as values for more granular control. + * User object that can be passed to `tracer.setUser()`. */ - measured?: boolean | { [key: string]: boolean }; -} - -declare namespace plugins { - /** @hidden */ - interface Integration { + export interface User { /** - * The service name to be used for this plugin. + * Unique identifier of the user. + * Mandatory. */ - service?: string | any; + id: string, - /** Whether to enable the plugin. - * @default true + /** + * Email of the user. */ - enabled?: boolean; - } + email?: string, - /** @hidden */ - interface Instrumentation extends Integration, Analyzable {} - - /** @hidden */ - interface Http extends Instrumentation { /** - * List of URLs that should be instrumented. - * - * @default /^.*$/ + * User-friendly name of the user. */ - allowlist?: string | RegExp | ((url: string) => boolean) | (string | RegExp | ((url: string) => boolean))[]; + name?: string, /** - * Deprecated in favor of `allowlist`. - * - * @deprecated - * @hidden + * Session ID of the user. */ - whitelist?: string | RegExp | ((url: string) => boolean) | (string | RegExp | ((url: string) => boolean))[]; + session_id?: string, /** - * List of URLs that should not be instrumented. Takes precedence over - * allowlist if a URL matches an entry in both. - * - * @default [] + * Role the user is making the request under. */ - blocklist?: string | RegExp | ((url: string) => boolean) | (string | RegExp | ((url: string) => boolean))[]; + role?: string, /** - * Deprecated in favor of `blocklist`. - * - * @deprecated - * @hidden + * Scopes or granted authorizations the user currently possesses. + * The value could come from the scope associated with an OAuth2 + * Access Token or an attribute value in a SAML 2 Assertion. */ - blacklist?: string | RegExp | ((url: string) => boolean) | (string | RegExp | ((url: string) => boolean))[]; + scope?: string, /** - * An array of headers to include in the span metadata. - * - * @default [] + * Custom fields to attach to the user (RBAC, Oauth, etc…). */ - headers?: string[]; + [key: string]: string | undefined + } + export interface DogStatsD { /** - * Callback function to determine if there was an error. It should take a - * status code as its only parameter and return `true` for success or `false` - * for errors. - * - * @default code => code < 500 + * Increments a metric by the specified value, optionally specifying tags. + * @param {string} stat The dot-separated metric name. + * @param {number} value The amount to increment the stat by. + * @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags. */ - validateStatus?: (code: number) => boolean; + increment(stat: string, value?: number, tags?: { [tag: string]: string|number }): void /** - * Enable injection of tracing headers into requests signed with AWS IAM headers. - * Disable this if you get AWS signature errors (HTTP 403). - * - * @default false + * Decrements a metric by the specified value, optionally specifying tags. + * @param {string} stat The dot-separated metric name. + * @param {number} value The amount to decrement the stat by. + * @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags. */ - enablePropagationWithAmazonHeaders?: boolean; - } + decrement(stat: string, value?: number, tags?: { [tag: string]: string|number }): void - /** @hidden */ - interface HttpServer extends Http { /** - * Callback function to determine if there was an error. It should take a - * status code as its only parameter and return `true` for success or `false` - * for errors. - * - * @default code => code < 500 + * Sets a distribution value, optionally specifying tags. + * @param {string} stat The dot-separated metric name. + * @param {number} value The amount to increment the stat by. + * @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags. */ - validateStatus?: (code: number) => boolean; + distribution(stat: string, value?: number, tags?: { [tag: string]: string|number }): void /** - * Hooks to run before spans are finished. + * Sets a gauge value, optionally specifying tags. + * @param {string} stat The dot-separated metric name. + * @param {number} value The amount to increment the stat by. + * @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags. */ - hooks?: { - /** - * Hook to execute just before the request span finishes. - */ - request?: (span?: Span, req?: IncomingMessage, res?: ServerResponse) => any; - }; + gauge(stat: string, value?: number, tags?: { [tag: string]: string|number }): void /** - * Whether to enable instrumentation of .middleware spans + * Forces any unsent metrics to be sent * - * @default true + * @beta This method is experimental and could be removed in future versions. */ - middleware?: boolean; + flush(): void } - /** @hidden */ - interface HttpClient extends Http { + export interface Appsec { /** - * Use the remote endpoint host as the service name instead of the default. + * Links a successful login event to the current trace. Will link the passed user to the current trace with Appsec.setUser() internally. + * @param {User} user Properties of the authenticated user. Accepts custom fields. + * @param {[key: string]: string} metadata Custom fields to link to the login success event. * - * @default false + * @beta This method is in beta and could change in future versions. */ - splitByDomain?: boolean; + trackUserLoginSuccessEvent(user: User, metadata?: { [key: string]: string }): void /** - * Callback function to determine if there was an error. It should take a - * status code as its only parameter and return `true` for success or `false` - * for errors. + * Links a failed login event to the current trace. + * @param {string} userId The user id of the attemped login. + * @param {boolean} exists If the user id exists. + * @param {[key: string]: string} metadata Custom fields to link to the login failure event. * - * @default code => code < 400 || code >= 500 + * @beta This method is in beta and could change in future versions. */ - validateStatus?: (code: number) => boolean; + trackUserLoginFailureEvent(userId: string, exists: boolean, metadata?: { [key: string]: string }): void /** - * Hooks to run before spans are finished. + * Links a custom event to the current trace. + * @param {string} eventName The name of the event. + * @param {[key: string]: string} metadata Custom fields to link to the event. + * + * @beta This method is in beta and could change in future versions. */ - hooks?: { - /** - * Hook to execute just before the request span finishes. - */ - request?: (span?: Span, req?: ClientRequest, res?: IncomingMessage) => any; - }; + trackCustomEvent(eventName: string, metadata?: { [key: string]: string }): void /** - * List of urls to which propagation headers should not be injected + * Checks if the passed user should be blocked according to AppSec rules. + * If no user is linked to the current trace, will link the passed user to it. + * @param {User} user Properties of the authenticated user. Accepts custom fields. + * @return {boolean} Indicates whether the user should be blocked. + * + * @beta This method is in beta and could change in the future */ - propagationBlocklist?: string | RegExp | ((url: string) => boolean) | (string | RegExp | ((url: string) => boolean))[]; - } + isUserBlocked(user: User): boolean - /** @hidden */ - interface Http2Client extends Http { /** - * Use the remote endpoint host as the service name instead of the default. + * Sends a "blocked" template response based on the request accept header and ends the response. + * **You should stop processing the request after calling this function!** + * @param {IncomingMessage} req Can be passed to force which request to act on. Optional. + * @param {OutgoingMessage} res Can be passed to force which response to act on. Optional. + * @return {boolean} Indicates if the action was successful. * - * @default false + * @beta This method is in beta and could change in the future */ - splitByDomain?: boolean; + blockRequest(req?: IncomingMessage, res?: OutgoingMessage): boolean /** - * Callback function to determine if there was an error. It should take a - * status code as its only parameter and return `true` for success or `false` - * for errors. + * Links an authenticated user to the current trace. + * @param {User} user Properties of the authenticated user. Accepts custom fields. * - * @default code => code < 400 || code >= 500 + * @beta This method is in beta and could change in the future */ - validateStatus?: (code: number) => boolean; + setUser(user: User): void } /** @hidden */ - interface Http2Server extends Http { + type anyObject = { + [key: string]: any; + }; + + /** @hidden */ + interface TransportRequestParams { + method: string; + path: string; + body?: anyObject; + bulkBody?: anyObject; + querystring?: anyObject; + } + + /** + * The Datadog Scope Manager. This is used for context propagation. + */ + export interface Scope { /** - * Callback function to determine if there was an error. It should take a - * status code as its only parameter and return `true` for success or `false` - * for errors. + * Get the current active span or null if there is none. * - * @default code => code < 500 + * @returns {Span} The active span. */ - validateStatus?: (code: number) => boolean; - } + active (): Span | null; - /** @hidden */ - interface Grpc extends Instrumentation { /** - * An array of metadata entries to record. Can also be a callback that returns - * the key/value pairs to record. For example, using - * `variables => variables` would record all variables. + * Activate a span in the scope of a function. + * + * @param {Span} span The span to activate. + * @param {Function} fn Function that will have the span activated on its scope. + * @returns The return value of the provided function. */ - metadata?: string[] | ((variables: { [key: string]: any }) => { [key: string]: any }); + activate (span: Span, fn: ((...args: any[]) => T)): T; + + /** + * Binds a target to the provided span, or the active span if omitted. + * + * @param {Function|Promise} fn Target that will have the span activated on its scope. + * @param {Span} [span=scope.active()] The span to activate. + * @returns The bound target. + */ + bind void> (fn: T, span?: Span | null): T; + bind V> (fn: T, span?: Span | null): T; + bind (fn: Promise, span?: Span | null): Promise; } /** @hidden */ - interface Moleculer extends Instrumentation { + interface Analyzable { /** - * Whether to include context meta as tags. - * - * @default false + * Whether to measure the span. Can also be set to a key-value pair with span + * names as keys and booleans as values for more granular control. */ - meta?: boolean; + measured?: boolean | { [key: string]: boolean }; } - /** - * This plugin automatically instruments the - * [amqp10](https://github.com/noodlefrenzy/node-amqp10) module. - */ - interface amqp10 extends Instrumentation {} + export namespace plugins { + /** @hidden */ + interface Integration { + /** + * The service name to be used for this plugin. + */ + service?: string | any; - /** - * This plugin automatically instruments the - * [amqplib](https://github.com/squaremo/amqp.node) module. - */ - interface amqplib extends Instrumentation {} + /** Whether to enable the plugin. + * @default true + */ + enabled?: boolean; + } - /** - * This plugin automatically instruments the - * [aws-sdk](https://github.com/aws/aws-sdk-js) module. - */ - interface aws_sdk extends Instrumentation { - /** - * Whether to add a suffix to the service name so that each AWS service has its own service name. - * @default true - */ - splitByAwsService?: boolean; + /** @hidden */ + interface Instrumentation extends Integration, Analyzable {} - /** - * Hooks to run before spans are finished. - */ - hooks?: { + /** @hidden */ + interface Http extends Instrumentation { /** - * Hook to execute just before the aws span finishes. + * List of URLs that should be instrumented. + * + * @default /^.*$/ */ - request?: (span?: Span, response?: anyObject) => any; - }; + allowlist?: string | RegExp | ((url: string) => boolean) | (string | RegExp | ((url: string) => boolean))[]; - /** - * Configuration for individual services to enable/disable them. Message - * queue services can also configure the producer and consumer individually - * by passing an object with a `producer` and `consumer` properties. The - * list of valid service keys is in the service-specific section of - * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html - */ - [key: string]: boolean | Object | undefined; - } + /** + * Deprecated in favor of `allowlist`. + * + * @deprecated + * @hidden + */ + whitelist?: string | RegExp | ((url: string) => boolean) | (string | RegExp | ((url: string) => boolean))[]; - /** - * This plugin patches the [bunyan](https://github.com/trentm/node-bunyan) - * to automatically inject trace identifiers in log records when the - * [logInjection](interfaces/traceroptions.html#logInjection) option is enabled - * on the tracer. - */ - interface bunyan extends Integration {} + /** + * List of URLs that should not be instrumented. Takes precedence over + * allowlist if a URL matches an entry in both. + * + * @default [] + */ + blocklist?: string | RegExp | ((url: string) => boolean) | (string | RegExp | ((url: string) => boolean))[]; - /** - * This plugin automatically instruments the - * [cassandra-driver](https://github.com/datastax/nodejs-driver) module. - */ - interface cassandra_driver extends Instrumentation {} + /** + * Deprecated in favor of `blocklist`. + * + * @deprecated + * @hidden + */ + blacklist?: string | RegExp | ((url: string) => boolean) | (string | RegExp | ((url: string) => boolean))[]; - /** - * This plugin automatically instruments the - * [connect](https://github.com/senchalabs/connect) module. - */ - interface connect extends HttpServer {} + /** + * An array of headers to include in the span metadata. + * + * @default [] + */ + headers?: string[]; - /** - * This plugin automatically instruments the - * [couchbase](https://www.npmjs.com/package/couchbase) module. - */ - interface couchbase extends Instrumentation {} + /** + * Callback function to determine if there was an error. It should take a + * status code as its only parameter and return `true` for success or `false` + * for errors. + * + * @default code => code < 500 + */ + validateStatus?: (code: number) => boolean; - /** - * This plugin automatically instruments the - * [cucumber](https://www.npmjs.com/package/@cucumber/cucumber) module. - */ - interface cucumber extends Integration {} + /** + * Enable injection of tracing headers into requests signed with AWS IAM headers. + * Disable this if you get AWS signature errors (HTTP 403). + * + * @default false + */ + enablePropagationWithAmazonHeaders?: boolean; + } - /** - * This plugin automatically instruments the - * [cypress](https://github.com/cypress-io/cypress) module. - */ - interface cypress extends Integration {} + /** @hidden */ + interface HttpServer extends Http { + /** + * Callback function to determine if there was an error. It should take a + * status code as its only parameter and return `true` for success or `false` + * for errors. + * + * @default code => code < 500 + */ + validateStatus?: (code: number) => boolean; - /** - * This plugin automatically instruments the - * [dns](https://nodejs.org/api/dns.html) module. - */ - interface dns extends Instrumentation {} + /** + * Hooks to run before spans are finished. + */ + hooks?: { + /** + * Hook to execute just before the request span finishes. + */ + request?: (span?: Span, req?: IncomingMessage, res?: ServerResponse) => any; + }; - /** - * This plugin automatically instruments the - * [elasticsearch](https://github.com/elastic/elasticsearch-js) module. - */ - interface elasticsearch extends Instrumentation { - /** - * Hooks to run before spans are finished. - */ - hooks?: { /** - * Hook to execute just before the query span finishes. + * Whether to enable instrumentation of .middleware spans + * + * @default true */ - query?: (span?: Span, params?: TransportRequestParams) => any; - }; - } + middleware?: boolean; + } - /** - * This plugin automatically instruments the - * [express](http://expressjs.com/) module. - */ - interface express extends HttpServer {} + /** @hidden */ + interface HttpClient extends Http { + /** + * Use the remote endpoint host as the service name instead of the default. + * + * @default false + */ + splitByDomain?: boolean; - /** - * This plugin automatically instruments the - * [fastify](https://www.fastify.io/) module. - */ - interface fastify extends HttpServer {} + /** + * Callback function to determine if there was an error. It should take a + * status code as its only parameter and return `true` for success or `false` + * for errors. + * + * @default code => code < 400 || code >= 500 + */ + validateStatus?: (code: number) => boolean; - /** - * This plugin automatically instruments the - * [fetch](https://nodejs.org/api/globals.html#fetch) global. - */ - interface fetch extends HttpClient {} + /** + * Hooks to run before spans are finished. + */ + hooks?: { + /** + * Hook to execute just before the request span finishes. + */ + request?: (span?: Span, req?: ClientRequest, res?: IncomingMessage) => any; + }; - /** - * This plugin patches the [generic-pool](https://github.com/coopernurse/node-pool) - * module to bind the callbacks the the caller context. - */ - interface generic_pool extends Integration {} + /** + * List of urls to which propagation headers should not be injected + */ + propagationBlocklist?: string | RegExp | ((url: string) => boolean) | (string | RegExp | ((url: string) => boolean))[]; + } - /** - * This plugin automatically instruments the - * [@google-cloud/pubsub](https://github.com/googleapis/nodejs-pubsub) module. - */ - interface google_cloud_pubsub extends Integration {} + /** @hidden */ + interface Http2Client extends Http { + /** + * Use the remote endpoint host as the service name instead of the default. + * + * @default false + */ + splitByDomain?: boolean; - /** @hidden */ - interface ExecutionArgs { - schema: any, - document: any, - rootValue?: any, - contextValue?: any, - variableValues?: any, - operationName?: string, - fieldResolver?: any, - typeResolver?: any, - } + /** + * Callback function to determine if there was an error. It should take a + * status code as its only parameter and return `true` for success or `false` + * for errors. + * + * @default code => code < 400 || code >= 500 + */ + validateStatus?: (code: number) => boolean; + } + + /** @hidden */ + interface Http2Server extends Http { + /** + * Callback function to determine if there was an error. It should take a + * status code as its only parameter and return `true` for success or `false` + * for errors. + * + * @default code => code < 500 + */ + validateStatus?: (code: number) => boolean; + } + + /** @hidden */ + interface Grpc extends Instrumentation { + /** + * An array of metadata entries to record. Can also be a callback that returns + * the key/value pairs to record. For example, using + * `variables => variables` would record all variables. + */ + metadata?: string[] | ((variables: { [key: string]: any }) => { [key: string]: any }); + } + + /** @hidden */ + interface Moleculer extends Instrumentation { + /** + * Whether to include context meta as tags. + * + * @default false + */ + meta?: boolean; + } - /** - * This plugin automatically instruments the - * [graphql](https://github.com/graphql/graphql-js) module. - * - * The `graphql` integration uses the operation name as the span resource name. - * If no operation name is set, the resource name will always be just `query`, - * `mutation` or `subscription`. - * - * For example: - * - * ```graphql - * # good, the resource name will be `query HelloWorld` - * query HelloWorld { - * hello - * world - * } - * - * # bad, the resource name will be `query` - * { - * hello - * world - * } - * ``` - */ - interface graphql extends Instrumentation { /** - * The maximum depth of fields/resolvers to instrument. Set to `0` to only - * instrument the operation or to `-1` to instrument all fields/resolvers. - * - * @default -1 + * This plugin automatically instruments the + * [amqp10](https://github.com/noodlefrenzy/node-amqp10) module. */ - depth?: number; + interface amqp10 extends Instrumentation {} /** - * Whether to include the source of the operation within the query as a tag - * on every span. This may contain sensitive information and sould only be - * enabled if sensitive data is always sent as variables and not in the - * query text. - * - * @default false + * This plugin automatically instruments the + * [amqplib](https://github.com/squaremo/amqp.node) module. */ - source?: boolean; + interface amqplib extends Instrumentation {} /** - * An array of variable names to record. Can also be a callback that returns - * the key/value pairs to record. For example, using - * `variables => variables` would record all variables. + * This plugin automatically instruments the + * [aws-sdk](https://github.com/aws/aws-sdk-js) module. */ - variables?: string[] | ((variables: { [key: string]: any }) => { [key: string]: any }); + interface aws_sdk extends Instrumentation { + /** + * Whether to add a suffix to the service name so that each AWS service has its own service name. + * @default true + */ + splitByAwsService?: boolean; + + /** + * Hooks to run before spans are finished. + */ + hooks?: { + /** + * Hook to execute just before the aws span finishes. + */ + request?: (span?: Span, response?: anyObject) => any; + }; + + /** + * Configuration for individual services to enable/disable them. Message + * queue services can also configure the producer and consumer individually + * by passing an object with a `producer` and `consumer` properties. The + * list of valid service keys is in the service-specific section of + * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html + */ + [key: string]: boolean | Object | undefined; + } /** - * Whether to collapse list items into a single element. (i.e. single - * `users.*.name` span instead of `users.0.name`, `users.1.name`, etc) - * - * @default true + * This plugin patches the [bunyan](https://github.com/trentm/node-bunyan) + * to automatically inject trace identifiers in log records when the + * [logInjection](interfaces/traceroptions.html#logInjection) option is enabled + * on the tracer. */ - collapse?: boolean; + interface bunyan extends Integration {} /** - * Whether to enable signature calculation for the resource name. This can - * be disabled if your GraphQL operations always have a name. Note that when - * disabled all queries will need to be named for this to work properly. - * - * @default true + * This plugin automatically instruments the + * [cassandra-driver](https://github.com/datastax/nodejs-driver) module. */ - signature?: boolean; + interface cassandra_driver extends Instrumentation {} /** - * An object of optional callbacks to be executed during the respective - * phase of a GraphQL operation. Undefined callbacks default to a noop - * function. - * - * @default {} + * This plugin automatically instruments the + * [connect](https://github.com/senchalabs/connect) module. */ - hooks?: { - execute?: (span?: Span, args?: ExecutionArgs, res?: any) => void; - validate?: (span?: Span, document?: any, errors?: any) => void; - parse?: (span?: Span, source?: any, document?: any) => void; - } - } + interface connect extends HttpServer {} - /** - * This plugin automatically instruments the - * [grpc](https://github.com/grpc/grpc-node) module. - */ - interface grpc extends Grpc { /** - * Configuration for gRPC clients. + * This plugin automatically instruments the + * [couchbase](https://www.npmjs.com/package/couchbase) module. */ - client?: Grpc, + interface couchbase extends Instrumentation {} /** - * Configuration for gRPC servers. + * This plugin automatically instruments the + * [cucumber](https://www.npmjs.com/package/@cucumber/cucumber) module. */ - server?: Grpc - } - - /** - * This plugin automatically instruments the - * [hapi](https://hapijs.com/) module. - */ - interface hapi extends HttpServer {} + interface cucumber extends Integration {} - /** - * This plugin automatically instruments the - * [http](https://nodejs.org/api/http.html) module. - * - * By default any option set at the root will apply to both clients and - * servers. To configure only one or the other, use the `client` and `server` - * options. - */ - interface http extends HttpClient, HttpServer { /** - * Configuration for HTTP clients. + * This plugin automatically instruments the + * [cypress](https://github.com/cypress-io/cypress) module. */ - client?: HttpClient | boolean, + interface cypress extends Integration {} /** - * Configuration for HTTP servers. + * This plugin automatically instruments the + * [dns](https://nodejs.org/api/dns.html) module. */ - server?: HttpServer | boolean + interface dns extends Instrumentation {} /** - * Hooks to run before spans are finished. + * This plugin automatically instruments the + * [elasticsearch](https://github.com/elastic/elasticsearch-js) module. */ - hooks?: { + interface elasticsearch extends Instrumentation { /** - * Hook to execute just before the request span finishes. + * Hooks to run before spans are finished. */ - request?: ( - span?: Span, - req?: IncomingMessage | ClientRequest, - res?: ServerResponse | IncomingMessage - ) => any; - }; - } + hooks?: { + /** + * Hook to execute just before the query span finishes. + */ + query?: (span?: Span, params?: TransportRequestParams) => any; + }; + } - /** - * This plugin automatically instruments the - * [http2](https://nodejs.org/api/http2.html) module. - * - * By default any option set at the root will apply to both clients and - * servers. To configure only one or the other, use the `client` and `server` - * options. - */ - interface http2 extends Http2Client, Http2Server { /** - * Configuration for HTTP clients. + * This plugin automatically instruments the + * [express](http://expressjs.com/) module. */ - client?: Http2Client | boolean, + interface express extends HttpServer {} /** - * Configuration for HTTP servers. + * This plugin automatically instruments the + * [fastify](https://www.fastify.io/) module. */ - server?: Http2Server | boolean - } + interface fastify extends HttpServer {} - /** - * This plugin automatically instruments the - * [ioredis](https://github.com/luin/ioredis) module. - */ - interface ioredis extends Instrumentation { /** - * List of commands that should be instrumented. - * - * @default /^.*$/ + * This plugin automatically instruments the + * [fetch](https://nodejs.org/api/globals.html#fetch) global. */ - allowlist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + interface fetch extends HttpClient {} /** - * Deprecated in favor of `allowlist`. - * - * @deprecated - * @hidden + * This plugin patches the [generic-pool](https://github.com/coopernurse/node-pool) + * module to bind the callbacks the the caller context. */ - whitelist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + interface generic_pool extends Integration {} /** - * List of commands that should not be instrumented. Takes precedence over - * allowlist if a command matches an entry in both. - * - * @default [] + * This plugin automatically instruments the + * [@google-cloud/pubsub](https://github.com/googleapis/nodejs-pubsub) module. */ - blocklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + interface google_cloud_pubsub extends Integration {} - /** - * Deprecated in favor of `blocklist`. - * - * @deprecated - * @hidden - */ - blacklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + /** @hidden */ + interface ExecutionArgs { + schema: any, + document: any, + rootValue?: any, + contextValue?: any, + variableValues?: any, + operationName?: string, + fieldResolver?: any, + typeResolver?: any, + } /** - * Whether to use a different service name for each Redis instance based - * on the configured connection name of the client. + * This plugin automatically instruments the + * [graphql](https://github.com/graphql/graphql-js) module. * - * @default false - */ - splitByInstance?: boolean; - } - - /** - * This plugin automatically instruments the - * [jest](https://github.com/facebook/jest) module. - */ - interface jest extends Integration {} - - /** - * This plugin patches the [knex](https://knexjs.org/) - * module to bind the promise callback the the caller context. - */ - interface knex extends Integration {} - - /** - * This plugin automatically instruments the - * [koa](https://koajs.com/) module. - */ - interface koa extends HttpServer {} - - /** - * This plugin automatically instruments the - * [kafkajs](https://kafka.js.org/) module. - */ - interface kafkajs extends Instrumentation {} - - /** - * This plugin automatically instruments the - * [ldapjs](https://github.com/ldapjs/node-ldapjs/) module. - */ - interface ldapjs extends Instrumentation {} + * The `graphql` integration uses the operation name as the span resource name. + * If no operation name is set, the resource name will always be just `query`, + * `mutation` or `subscription`. + * + * For example: + * + * ```graphql + * # good, the resource name will be `query HelloWorld` + * query HelloWorld { + * hello + * world + * } + * + * # bad, the resource name will be `query` + * { + * hello + * world + * } + * ``` + */ + interface graphql extends Instrumentation { + /** + * The maximum depth of fields/resolvers to instrument. Set to `0` to only + * instrument the operation or to `-1` to instrument all fields/resolvers. + * + * @default -1 + */ + depth?: number; - /** - * This plugin automatically instruments the - * [mariadb](https://github.com/mariadb-corporation/mariadb-connector-nodejs) module. - */ - interface mariadb extends mysql {} + /** + * Whether to include the source of the operation within the query as a tag + * on every span. This may contain sensitive information and sould only be + * enabled if sensitive data is always sent as variables and not in the + * query text. + * + * @default false + */ + source?: boolean; - /** - * This plugin automatically instruments the - * [memcached](https://github.com/3rd-Eden/memcached) module. - */ - interface memcached extends Instrumentation {} + /** + * An array of variable names to record. Can also be a callback that returns + * the key/value pairs to record. For example, using + * `variables => variables` would record all variables. + */ + variables?: string[] | ((variables: { [key: string]: any }) => { [key: string]: any }); - /** - * This plugin automatically instruments the - * [microgateway-core](https://github.com/apigee/microgateway-core) module. - */ - interface microgateway_core extends HttpServer {} + /** + * Whether to collapse list items into a single element. (i.e. single + * `users.*.name` span instead of `users.0.name`, `users.1.name`, etc) + * + * @default true + */ + collapse?: boolean; - /** - * This plugin automatically instruments the - * [mocha](https://mochajs.org/) module. - */ - interface mocha extends Integration {} + /** + * Whether to enable signature calculation for the resource name. This can + * be disabled if your GraphQL operations always have a name. Note that when + * disabled all queries will need to be named for this to work properly. + * + * @default true + */ + signature?: boolean; + + /** + * An object of optional callbacks to be executed during the respective + * phase of a GraphQL operation. Undefined callbacks default to a noop + * function. + * + * @default {} + */ + hooks?: { + execute?: (span?: Span, args?: ExecutionArgs, res?: any) => void; + validate?: (span?: Span, document?: any, errors?: any) => void; + parse?: (span?: Span, source?: any, document?: any) => void; + } + } - /** - * This plugin automatically instruments the - * [moleculer](https://moleculer.services/) module. - */ - interface moleculer extends Moleculer { /** - * Configuration for Moleculer clients. Set to false to disable client - * instrumentation. + * This plugin automatically instruments the + * [grpc](https://github.com/grpc/grpc-node) module. */ - client?: boolean | Moleculer; + interface grpc extends Grpc { + /** + * Configuration for gRPC clients. + */ + client?: Grpc, + + /** + * Configuration for gRPC servers. + */ + server?: Grpc + } /** - * Configuration for Moleculer servers. Set to false to disable server - * instrumentation. + * This plugin automatically instruments the + * [hapi](https://hapijs.com/) module. */ - server?: boolean | Moleculer; - } + interface hapi extends HttpServer {} - /** - * This plugin automatically instruments the - * [mongodb-core](https://github.com/mongodb-js/mongodb-core) module. - */ - interface mongodb_core extends Instrumentation { /** - * Whether to include the query contents in the resource name. + * This plugin automatically instruments the + * [http](https://nodejs.org/api/http.html) module. + * + * By default any option set at the root will apply to both clients and + * servers. To configure only one or the other, use the `client` and `server` + * options. */ - queryInResourceName?: boolean; - } - - /** - * This plugin automatically instruments the - * [mongoose](https://mongoosejs.com/) module. - */ - interface mongoose extends Instrumentation {} - - /** - * This plugin automatically instruments the - * [mysql](https://github.com/mysqljs/mysql) module. - */ - interface mysql extends Instrumentation { - service?: string | ((params: any) => string); - } + interface http extends HttpClient, HttpServer { + /** + * Configuration for HTTP clients. + */ + client?: HttpClient | boolean, - /** - * This plugin automatically instruments the - * [mysql2](https://github.com/sidorares/node-mysql2) module. - */ - interface mysql2 extends mysql {} + /** + * Configuration for HTTP servers. + */ + server?: HttpServer | boolean - /** - * This plugin automatically instruments the - * [net](https://nodejs.org/api/net.html) module. - */ - interface net extends Instrumentation {} + /** + * Hooks to run before spans are finished. + */ + hooks?: { + /** + * Hook to execute just before the request span finishes. + */ + request?: ( + span?: Span, + req?: IncomingMessage | ClientRequest, + res?: ServerResponse | IncomingMessage + ) => any; + }; + } - /** - * This plugin automatically instruments the - * [next](https://nextjs.org/) module. - */ - interface next extends Instrumentation { /** - * Hooks to run before spans are finished. + * This plugin automatically instruments the + * [http2](https://nodejs.org/api/http2.html) module. + * + * By default any option set at the root will apply to both clients and + * servers. To configure only one or the other, use the `client` and `server` + * options. */ - hooks?: { + interface http2 extends Http2Client, Http2Server { /** - * Hook to execute just before the request span finishes. + * Configuration for HTTP clients. */ - request?: (span?: Span, req?: IncomingMessage, res?: ServerResponse) => any; - }; - } - - /** - * This plugin automatically instruments the - * [openai](https://platform.openai.com/docs/api-reference?lang=node.js) module. - * - * Note that for logs to work you'll need to set the `DD_API_KEY` environment variable. - * You'll also need to adjust any firewall settings to allow the tracer to communicate - * with `http-intake.logs.datadoghq.com`. - * - * Note that for metrics to work you'll need to enable - * [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/?tab=hostagent#setup) - * in the agent. - */ - interface openai extends Instrumentation {} + client?: Http2Client | boolean, - /** - * This plugin automatically instruments the - * [opensearch](https://github.com/opensearch-project/opensearch-js) module. - */ - interface opensearch extends elasticsearch {} + /** + * Configuration for HTTP servers. + */ + server?: Http2Server | boolean + } - /** - * This plugin automatically instruments the - * [oracledb](https://github.com/oracle/node-oracledb) module. - */ - interface oracledb extends Instrumentation { /** - * The service name to be used for this plugin. If a function is used, it will be passed the connection parameters and its return value will be used as the service name. + * This plugin automatically instruments the + * [ioredis](https://github.com/luin/ioredis) module. */ - service?: string | ((params: any) => string); - } + interface ioredis extends Instrumentation { + /** + * List of commands that should be instrumented. Commands must be in + * lowercase for example 'xread'. + * + * @default /^.*$/ + */ + allowlist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** - * This plugin automatically instruments the - * [paperplane](https://github.com/articulate/paperplane) module. - */ - interface paperplane extends HttpServer {} + /** + * Deprecated in favor of `allowlist`. + * + * @deprecated + * @hidden + */ + whitelist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** - * This plugin automatically instruments the - * [playwright](https://github.com/microsoft/playwright) module. - */ - interface playwright extends Integration {} + /** + * List of commands that should not be instrumented. Takes precedence over + * allowlist if a command matches an entry in both. Commands must be in + * lowercase for example 'xread'. + * + * @default [] + */ + blocklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + + /** + * Deprecated in favor of `blocklist`. + * + * @deprecated + * @hidden + */ + blacklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + + /** + * Whether to use a different service name for each Redis instance based + * on the configured connection name of the client. + * + * @default false + */ + splitByInstance?: boolean; + } - /** - * This plugin automatically instruments the - * [pg](https://node-postgres.com/) module. - */ - interface pg extends Instrumentation { /** - * The service name to be used for this plugin. If a function is used, it will be passed the connection parameters and its return value will be used as the service name. + * This plugin automatically instruments the + * [jest](https://github.com/facebook/jest) module. */ - service?: string | ((params: any) => string); - } + interface jest extends Integration {} - /** - * This plugin patches the [pino](http://getpino.io) - * to automatically inject trace identifiers in log records when the - * [logInjection](interfaces/traceroptions.html#logInjection) option is enabled - * on the tracer. - */ - interface pino extends Integration {} + /** + * This plugin patches the [knex](https://knexjs.org/) + * module to bind the promise callback the the caller context. + */ + interface knex extends Integration {} - /** - * This plugin automatically instruments the - * [redis](https://github.com/NodeRedis/node_redis) module. - */ - interface redis extends Instrumentation { /** - * List of commands that should be instrumented. - * - * @default /^.*$/ + * This plugin automatically instruments the + * [koa](https://koajs.com/) module. */ - allowlist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + interface koa extends HttpServer {} /** - * Deprecated in favor of `allowlist`. - * - * deprecated - * @hidden + * This plugin automatically instruments the + * [kafkajs](https://kafka.js.org/) module. */ - whitelist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + interface kafkajs extends Instrumentation {} /** - * List of commands that should not be instrumented. Takes precedence over - * allowlist if a command matches an entry in both. - * - * @default [] + * This plugin automatically instruments the + * [ldapjs](https://github.com/ldapjs/node-ldapjs/) module. */ - blocklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + interface ldapjs extends Instrumentation {} /** - * Deprecated in favor of `blocklist`. - * - * @deprecated - * @hidden + * This plugin automatically instruments the + * [mariadb](https://github.com/mariadb-corporation/mariadb-connector-nodejs) module. */ - blacklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - } + interface mariadb extends mysql {} - /** - * This plugin automatically instruments the - * [restify](http://restify.com/) module. - */ - interface restify extends HttpServer {} + /** + * This plugin automatically instruments the + * [memcached](https://github.com/3rd-Eden/memcached) module. + */ + interface memcached extends Instrumentation {} - /** - * This plugin automatically instruments the - * [rhea](https://github.com/amqp/rhea) module. - */ - interface rhea extends Instrumentation {} + /** + * This plugin automatically instruments the + * [microgateway-core](https://github.com/apigee/microgateway-core) module. + */ + interface microgateway_core extends HttpServer {} - /** - * This plugin automatically instruments the - * [router](https://github.com/pillarjs/router) module. - */ - interface router extends Integration {} + /** + * This plugin automatically instruments the + * [mocha](https://mochajs.org/) module. + */ + interface mocha extends Integration {} - /** - * This plugin automatically instruments the - * [sharedb](https://github.com/share/sharedb) module. - */ - interface sharedb extends Integration { /** - * Hooks to run before spans are finished. + * This plugin automatically instruments the + * [moleculer](https://moleculer.services/) module. */ - hooks?: { + interface moleculer extends Moleculer { /** - * Hook to execute just when the span is created. + * Configuration for Moleculer clients. Set to false to disable client + * instrumentation. */ - receive?: (span?: Span, request?: any) => any; + client?: boolean | Moleculer; /** - * Hook to execute just when the span is finished. + * Configuration for Moleculer servers. Set to false to disable server + * instrumentation. */ - reply?: (span?: Span, request?: any, response?: any) => any; - }; - } + server?: boolean | Moleculer; + } - /** - * This plugin automatically instruments the - * [tedious](https://github.com/tediousjs/tedious/) module. - */ - interface tedious extends Instrumentation {} + /** + * This plugin automatically instruments the + * [mongodb-core](https://github.com/mongodb-js/mongodb-core) module. + */ + interface mongodb_core extends Instrumentation { + /** + * Whether to include the query contents in the resource name. + */ + queryInResourceName?: boolean; + } - /** - * This plugin patches the [winston](https://github.com/winstonjs/winston) - * to automatically inject trace identifiers in log records when the - * [logInjection](interfaces/traceroptions.html#logInjection) option is enabled - * on the tracer. - */ - interface winston extends Integration {} -} + /** + * This plugin automatically instruments the + * [mongoose](https://mongoosejs.com/) module. + */ + interface mongoose extends Instrumentation {} -export namespace opentelemetry { - /** - * A registry for creating named {@link Tracer}s. - */ - export interface TracerProvider extends otel.TracerProvider { /** - * Construct a new TracerProvider to register with @opentelemetry/api - * - * @returns TracerProvider A TracerProvider instance + * This plugin automatically instruments the + * [mysql](https://github.com/mysqljs/mysql) module. */ - new(): TracerProvider; + interface mysql extends Instrumentation { + service?: string | ((params: any) => string); + } /** - * Returns a Tracer, creating one if one with the given name and version is - * not already created. - * - * This function may return different Tracer types (e.g. - * {@link NoopTracerProvider} vs. a functional tracer). - * - * @param name The name of the tracer or instrumentation library. - * @param version The version of the tracer or instrumentation library. - * @param options The options of the tracer or instrumentation library. - * @returns Tracer A Tracer with the given name and version + * This plugin automatically instruments the + * [mysql2](https://github.com/sidorares/node-mysql2) module. */ - getTracer(name: string, version?: string): Tracer; + interface mysql2 extends mysql {} /** - * Register this tracer provider with @opentelemetry/api + * This plugin automatically instruments the + * [net](https://nodejs.org/api/net.html) module. */ - register(): void; - } + interface net extends Instrumentation {} - /** - * Tracer provides an interface for creating {@link Span}s. - */ - export interface Tracer extends otel.Tracer { /** - * Starts a new {@link Span}. Start the span without setting it on context. - * - * This method do NOT modify the current Context. - * - * @param name The name of the span - * @param [options] SpanOptions used for span creation - * @param [context] Context to use to extract parent - * @returns Span The newly created span - * @example - * const span = tracer.startSpan('op'); - * span.setAttribute('key', 'value'); - * span.end(); - */ - startSpan(name: string, options?: SpanOptions, context?: Context): Span; - - /** - * Starts a new {@link Span} and calls the given function passing it the - * created span as first argument. - * Additionally the new span gets set in context and this context is activated - * for the duration of the function call. - * - * @param name The name of the span - * @param [options] SpanOptions used for span creation - * @param [context] Context to use to extract parent - * @param fn function called in the context of the span and receives the newly created span as an argument - * @returns return value of fn - * @example - * const something = tracer.startActiveSpan('op', span => { - * try { - * do some work - * span.setStatus({code: SpanStatusCode.OK}); - * return something; - * } catch (err) { - * span.setStatus({ - * code: SpanStatusCode.ERROR, - * message: err.message, - * }); - * throw err; - * } finally { - * span.end(); - * } - * }); - * - * @example - * const span = tracer.startActiveSpan('op', span => { - * try { - * do some work - * return span; - * } catch (err) { - * span.setStatus({ - * code: SpanStatusCode.ERROR, - * message: err.message, - * }); - * throw err; - * } - * }); - * do some more work - * span.end(); - */ - startActiveSpan unknown>(name: string, fn: F): ReturnType; - startActiveSpan unknown>(name: string, options: SpanOptions, fn: F): ReturnType; - startActiveSpan unknown>(name: string, options: SpanOptions, context: otel.Context, fn: F): ReturnType; - } + * This plugin automatically instruments the + * [next](https://nextjs.org/) module. + */ + interface next extends Instrumentation { + /** + * Hooks to run before spans are finished. + */ + hooks?: { + /** + * Hook to execute just before the request span finishes. + */ + request?: (span?: Span, req?: IncomingMessage, res?: ServerResponse) => any; + }; + } - /** - * An interface that represents a span. A span represents a single operation - * within a trace. Examples of span might include remote procedure calls or a - * in-process function calls to sub-components. A Trace has a single, top-level - * "root" Span that in turn may have zero or more child Spans, which in turn - * may have children. - * - * Spans are created by the {@link Tracer.startSpan} method. - */ - export interface Span extends otel.Span { /** - * Returns the {@link SpanContext} object associated with this Span. + * This plugin automatically instruments the + * [openai](https://platform.openai.com/docs/api-reference?lang=node.js) module. * - * Get an immutable, serializable identifier for this span that can be used - * to create new child spans. Returned SpanContext is usable even after the - * span ends. + * Note that for logs to work you'll need to set the `DD_API_KEY` environment variable. + * You'll also need to adjust any firewall settings to allow the tracer to communicate + * with `http-intake.logs.datadoghq.com`. * - * @returns the SpanContext object associated with this Span. + * Note that for metrics to work you'll need to enable + * [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/?tab=hostagent#setup) + * in the agent. */ - spanContext(): SpanContext; + interface openai extends Instrumentation {} /** - * Sets an attribute to the span. - * - * Sets a single Attribute with the key and value passed as arguments. - * - * @param key the key for this attribute. - * @param value the value for this attribute. Setting a value null or - * undefined is invalid and will result in undefined behavior. + * This plugin automatically instruments the + * [opensearch](https://github.com/opensearch-project/opensearch-js) module. */ - setAttribute(key: string, value: SpanAttributeValue): this; + interface opensearch extends elasticsearch {} /** - * Sets attributes to the span. - * - * @param attributes the attributes that will be added. - * null or undefined attribute values - * are invalid and will result in undefined behavior. + * This plugin automatically instruments the + * [oracledb](https://github.com/oracle/node-oracledb) module. */ - setAttributes(attributes: SpanAttributes): this; + interface oracledb extends Instrumentation { + /** + * The service name to be used for this plugin. If a function is used, it will be passed the connection parameters and its return value will be used as the service name. + */ + service?: string | ((params: any) => string); + } /** - * Adds an event to the Span. - * - * @param name the name of the event. - * @param [attributesOrStartTime] the attributes that will be added; these are - * associated with this event. Can be also a start time - * if type is {@type TimeInput} and 3rd param is undefined - * @param [startTime] start time of the event. + * This plugin automatically instruments the + * [paperplane](https://github.com/articulate/paperplane) module. */ - addEvent(name: string, attributesOrStartTime?: SpanAttributes | TimeInput, startTime?: TimeInput): this; + interface paperplane extends HttpServer {} /** - * Sets a status to the span. If used, this will override the default Span - * status. Default is {@link SpanStatusCode.UNSET}. SetStatus overrides the value - * of previous calls to SetStatus on the Span. - * - * @param status the SpanStatus to set. + * This plugin automatically instruments the + * [playwright](https://github.com/microsoft/playwright) module. */ - setStatus(status: SpanStatus): this; + interface playwright extends Integration {} /** - * Updates the Span name. - * - * This will override the name provided via {@link Tracer.startSpan}. - * - * Upon this update, any sampling behavior based on Span name will depend on - * the implementation. - * - * @param name the Span name. + * This plugin automatically instruments the + * [pg](https://node-postgres.com/) module. */ - updateName(name: string): this; + interface pg extends Instrumentation { + /** + * The service name to be used for this plugin. If a function is used, it will be passed the connection parameters and its return value will be used as the service name. + */ + service?: string | ((params: any) => string); + } /** - * Marks the end of Span execution. - * - * Call to End of a Span MUST not have any effects on child spans. Those may - * still be running and can be ended later. - * - * Do not return `this`. The Span generally should not be used after it - * is ended so chaining is not desired in this context. - * - * @param [endTime] the time to set as Span's end time. If not provided, - * use the current time as the span's end time. + * This plugin patches the [pino](http://getpino.io) + * to automatically inject trace identifiers in log records when the + * [logInjection](interfaces/traceroptions.html#logInjection) option is enabled + * on the tracer. */ - end(endTime?: TimeInput): void; + interface pino extends Integration {} /** - * Returns the flag whether this span will be recorded. - * - * @returns true if this Span is active and recording information like events - * with the `AddEvent` operation and attributes using `setAttributes`. + * This plugin automatically instruments the + * [redis](https://github.com/NodeRedis/node_redis) module. */ - isRecording(): boolean; + interface redis extends Instrumentation { + /** + * List of commands that should be instrumented. + * + * @default /^.*$/ + */ + allowlist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + + /** + * Deprecated in favor of `allowlist`. + * + * deprecated + * @hidden + */ + whitelist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + + /** + * List of commands that should not be instrumented. Takes precedence over + * allowlist if a command matches an entry in both. + * + * @default [] + */ + blocklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + + /** + * Deprecated in favor of `blocklist`. + * + * @deprecated + * @hidden + */ + blacklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; + } /** - * Sets exception as a span event - * @param exception the exception the only accepted values are string or Error - * @param [time] the time to set as Span's event time. If not provided, - * use the current time. + * This plugin automatically instruments the + * [restify](http://restify.com/) module. */ - recordException(exception: Exception, time?: TimeInput): void; + interface restify extends HttpServer {} /** - * Causally links another span to the current span - * @param {otel.SpanContext} context The context of the span to link to. - * @param {SpanAttributes} attributes An optional key value pair of arbitrary values. - * @returns {void} + * This plugin automatically instruments the + * [rhea](https://github.com/amqp/rhea) module. */ - addLink (context: otel.SpanContext, attributes?: SpanAttributes): void; - } + interface rhea extends Instrumentation {} - /** - * A SpanContext represents the portion of a {@link Span} which must be - * serialized and propagated along side of a {@link Baggage}. - */ - export interface SpanContext extends otel.SpanContext { /** - * The ID of the trace that this span belongs to. It is worldwide unique - * with practically sufficient probability by being made as 16 randomly - * generated bytes, encoded as a 32 lowercase hex characters corresponding to - * 128 bits. + * This plugin automatically instruments the + * [router](https://github.com/pillarjs/router) module. */ - traceId: string; + interface router extends Integration {} /** - * The ID of the Span. It is globally unique with practically sufficient - * probability by being made as 8 randomly generated bytes, encoded as a 16 - * lowercase hex characters corresponding to 64 bits. + * This plugin automatically instruments the + * [sharedb](https://github.com/share/sharedb) module. */ - spanId: string; + interface sharedb extends Integration { + /** + * Hooks to run before spans are finished. + */ + hooks?: { + /** + * Hook to execute just when the span is created. + */ + receive?: (span?: Span, request?: any) => any; + + /** + * Hook to execute just when the span is finished. + */ + reply?: (span?: Span, request?: any, response?: any) => any; + }; + } /** - * Only true if the SpanContext was propagated from a remote parent. + * This plugin automatically instruments the + * [tedious](https://github.com/tediousjs/tedious/) module. */ - isRemote?: boolean; + interface tedious extends Instrumentation {} /** - * Trace flags to propagate. - * - * It is represented as 1 byte (bitmap). Bit to represent whether trace is - * sampled or not. When set, the least significant bit documents that the - * caller may have recorded trace data. A caller who does not record trace - * data out-of-band leaves this flag unset. - * - * see {@link TraceFlags} for valid flag values. + * This plugin patches the [winston](https://github.com/winstonjs/winston) + * to automatically inject trace identifiers in log records when the + * [logInjection](interfaces/traceroptions.html#logInjection) option is enabled + * on the tracer. */ - traceFlags: number; + interface winston extends Integration {} + } + export namespace opentelemetry { /** - * Tracing-system-specific info to propagate. - * - * The tracestate field value is a `list` as defined below. The `list` is a - * series of `list-members` separated by commas `,`, and a list-member is a - * key/value pair separated by an equals sign `=`. Spaces and horizontal tabs - * surrounding `list-members` are ignored. There can be a maximum of 32 - * `list-members` in a `list`. - * More Info: https://www.w3.org/TR/trace-context/#tracestate-field + * A registry for creating named {@link Tracer}s. + */ + export interface TracerProvider extends otel.TracerProvider { + /** + * Construct a new TracerProvider to register with @opentelemetry/api + * + * @returns TracerProvider A TracerProvider instance + */ + new(): TracerProvider; + + /** + * Returns a Tracer, creating one if one with the given name and version is + * not already created. + * + * @param name The name of the tracer or instrumentation library. + * @param version The version of the tracer or instrumentation library. + * @param options The options of the tracer or instrumentation library. + * @returns Tracer A Tracer with the given name and version + */ + getTracer(name: string, version?: string, options?: any): Tracer; + + /** + * Register this tracer provider with @opentelemetry/api + */ + register(): void; + } + + /** + * Tracer provides an interface for creating {@link Span}s. + */ + export interface Tracer extends otel.Tracer { + /** + * Starts a new {@link Span}. Start the span without setting it on context. + * + * This method do NOT modify the current Context. + * + * @param name The name of the span + * @param [options] SpanOptions used for span creation + * @param [context] Context to use to extract parent + * @returns Span The newly created span + * @example + * const span = tracer.startSpan('op'); + * span.setAttribute('key', 'value'); + * span.end(); + */ + startSpan(name: string, options?: SpanOptions, context?: Context): Span; + + /** + * Starts a new {@link Span} and calls the given function passing it the + * created span as first argument. + * Additionally the new span gets set in context and this context is activated + * for the duration of the function call. + * + * @param name The name of the span + * @param [options] SpanOptions used for span creation + * @param [context] Context to use to extract parent + * @param fn function called in the context of the span and receives the newly created span as an argument + * @returns return value of fn + * @example + * const something = tracer.startActiveSpan('op', span => { + * try { + * do some work + * span.setStatus({code: SpanStatusCode.OK}); + * return something; + * } catch (err) { + * span.setStatus({ + * code: SpanStatusCode.ERROR, + * message: err.message, + * }); + * throw err; + * } finally { + * span.end(); + * } + * }); + * + * @example + * const span = tracer.startActiveSpan('op', span => { + * try { + * do some work + * return span; + * } catch (err) { + * span.setStatus({ + * code: SpanStatusCode.ERROR, + * message: err.message, + * }); + * throw err; + * } + * }); + * do some more work + * span.end(); + */ + startActiveSpan unknown>(name: string, options: SpanOptions, context: otel.Context, fn: F): ReturnType; + startActiveSpan unknown>(name: string, options: SpanOptions, fn: F): ReturnType; + startActiveSpan unknown>(name: string, fn: F): ReturnType; + } + + /** + * An interface that represents a span. A span represents a single operation + * within a trace. Examples of span might include remote procedure calls or a + * in-process function calls to sub-components. A Trace has a single, top-level + * "root" Span that in turn may have zero or more child Spans, which in turn + * may have children. * - * Examples: - * Single tracing system (generic format): - * tracestate: rojo=00f067aa0ba902b7 - * Multiple tracing systems (with different formatting): - * tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE + * Spans are created by the {@link Tracer.startSpan} method. */ - traceState?: TraceState; - } + export interface Span extends otel.Span { + /** + * Returns the {@link SpanContext} object associated with this Span. + * + * Get an immutable, serializable identifier for this span that can be used + * to create new child spans. Returned SpanContext is usable even after the + * span ends. + * + * @returns the SpanContext object associated with this Span. + */ + spanContext(): SpanContext; + + /** + * Sets an attribute to the span. + * + * Sets a single Attribute with the key and value passed as arguments. + * + * @param key the key for this attribute. + * @param value the value for this attribute. Setting a value null or + * undefined is invalid and will result in undefined behavior. + */ + setAttribute(key: string, value: SpanAttributeValue): this; + + /** + * Sets attributes to the span. + * + * @param attributes the attributes that will be added. + * null or undefined attribute values + * are invalid and will result in undefined behavior. + */ + setAttributes(attributes: SpanAttributes): this; + + /** + * Adds an event to the Span. + * + * @param name the name of the event. + * @param [attributesOrStartTime] the attributes that will be added; these are + * associated with this event. Can be also a start time + * if type is {@link TimeInput} and 3rd param is undefined + * @param [startTime] start time of the event. + */ + addEvent(name: string, attributesOrStartTime?: SpanAttributes | TimeInput, startTime?: TimeInput): this; + + /** + * Sets a status to the span. If used, this will override the default Span + * status. Default is {@link otel.SpanStatusCode.UNSET}. SetStatus overrides the value + * of previous calls to SetStatus on the Span. + * + * @param status the SpanStatus to set. + */ + setStatus(status: SpanStatus): this; + + /** + * Updates the Span name. + * + * This will override the name provided via {@link Tracer.startSpan}. + * + * Upon this update, any sampling behavior based on Span name will depend on + * the implementation. + * + * @param name the Span name. + */ + updateName(name: string): this; - export type Context = otel.Context; - export type Exception = otel.Exception; - export type SpanAttributes = otel.SpanAttributes; - export type SpanAttributeValue = otel.SpanAttributeValue; - export type SpanOptions = otel.SpanOptions; - export type SpanStatus = otel.SpanStatus; - export type TimeInput = otel.TimeInput; - export type TraceState = otel.TraceState; + /** + * Marks the end of Span execution. + * + * Call to End of a Span MUST not have any effects on child spans. Those may + * still be running and can be ended later. + * + * Do not return `this`. The Span generally should not be used after it + * is ended so chaining is not desired in this context. + * + * @param [endTime] the time to set as Span's end time. If not provided, + * use the current time as the span's end time. + */ + end(endTime?: TimeInput): void; + + /** + * Returns the flag whether this span will be recorded. + * + * @returns true if this Span is active and recording information like events + * with the `AddEvent` operation and attributes using `setAttributes`. + */ + isRecording(): boolean; + + /** + * Sets exception as a span event + * @param exception the exception the only accepted values are string or Error + * @param [time] the time to set as Span's event time. If not provided, + * use the current time. + */ + recordException(exception: Exception, time?: TimeInput): void; + + /** + * Causally links another span to the current span + * @param {otel.SpanContext} context The context of the span to link to. + * @param {SpanAttributes} attributes An optional key value pair of arbitrary values. + * @returns {void} + */ + addLink(context: otel.SpanContext, attributes?: SpanAttributes): void; + } + + /** + * A SpanContext represents the portion of a {@link Span} which must be + * serialized and propagated along side of a {@link otel.Baggage}. + */ + export interface SpanContext extends otel.SpanContext { + /** + * The ID of the trace that this span belongs to. It is worldwide unique + * with practically sufficient probability by being made as 16 randomly + * generated bytes, encoded as a 32 lowercase hex characters corresponding to + * 128 bits. + */ + traceId: string; + + /** + * The ID of the Span. It is globally unique with practically sufficient + * probability by being made as 8 randomly generated bytes, encoded as a 16 + * lowercase hex characters corresponding to 64 bits. + */ + spanId: string; + + /** + * Only true if the SpanContext was propagated from a remote parent. + */ + isRemote?: boolean; + + /** + * Trace flags to propagate. + * + * It is represented as 1 byte (bitmap). Bit to represent whether trace is + * sampled or not. When set, the least significant bit documents that the + * caller may have recorded trace data. A caller who does not record trace + * data out-of-band leaves this flag unset. + * + * see {@link otel.TraceFlags} for valid flag values. + */ + traceFlags: number; + + /** + * Tracing-system-specific info to propagate. + * + * The tracestate field value is a `list` as defined below. The `list` is a + * series of `list-members` separated by commas `,`, and a list-member is a + * key/value pair separated by an equals sign `=`. Spaces and horizontal tabs + * surrounding `list-members` are ignored. There can be a maximum of 32 + * `list-members` in a `list`. + * More Info: https://www.w3.org/TR/trace-context/#tracestate-field + * + * Examples: + * Single tracing system (generic format): + * tracestate: rojo=00f067aa0ba902b7 + * Multiple tracing systems (with different formatting): + * tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE + */ + traceState?: TraceState; + } + + export type Context = otel.Context; + export type Exception = otel.Exception; + export type SpanAttributes = otel.SpanAttributes; + export type SpanAttributeValue = otel.SpanAttributeValue; + export type SpanOptions = otel.SpanOptions; + export type SpanStatus = otel.SpanStatus; + export type TimeInput = otel.TimeInput; + export type TraceState = otel.TraceState; + } } /** @@ -1994,6 +2007,6 @@ export namespace opentelemetry { * start tracing. If not initialized, or initialized and disabled, it will use * a no-op implementation. */ -export declare const tracer: Tracer; +declare const tracer: Tracer; -export default tracer; +export = tracer; diff --git a/integration-tests/ci-visibility-intake.js b/integration-tests/ci-visibility-intake.js index 2674579541b..e140104712f 100644 --- a/integration-tests/ci-visibility-intake.js +++ b/integration-tests/ci-visibility-intake.js @@ -187,7 +187,7 @@ class FakeCiVisIntake extends FakeAgent { const data = JSON.stringify({ data: { attributes: { - test_full_names: knownTests + tests: knownTests } } }) diff --git a/integration-tests/ci-visibility.spec.js b/integration-tests/ci-visibility.spec.js index 6c037553d62..15fdb903536 100644 --- a/integration-tests/ci-visibility.spec.js +++ b/integration-tests/ci-visibility.spec.js @@ -29,7 +29,8 @@ const { TEST_IS_NEW, TEST_EARLY_FLAKE_IS_RETRY, TEST_EARLY_FLAKE_IS_ENABLED, - TEST_NAME + TEST_NAME, + JEST_DISPLAY_NAME } = require('../packages/dd-trace/src/plugins/util/test') const { ERROR_MESSAGE } = require('../packages/dd-trace/src/constants') @@ -598,6 +599,41 @@ testFrameworks.forEach(({ }).catch(done) }) }) + it('grabs the jest displayName config and sets tag in tests and suites', (done) => { + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + assert.equal(tests.length, 4) // two per display name + const nodeTests = tests.filter(test => test.meta[JEST_DISPLAY_NAME] === 'node') + assert.equal(nodeTests.length, 2) + + const standardTests = tests.filter(test => test.meta[JEST_DISPLAY_NAME] === 'standard') + assert.equal(standardTests.length, 2) + + const suites = events.filter(event => event.type === 'test_suite_end').map(event => event.content) + assert.equal(suites.length, 4) + + const nodeSuites = suites.filter(suite => suite.meta[JEST_DISPLAY_NAME] === 'node') + assert.equal(nodeSuites.length, 2) + + const standardSuites = suites.filter(suite => suite.meta[JEST_DISPLAY_NAME] === 'standard') + assert.equal(standardSuites.length, 2) + }) + childProcess = exec( + 'node ./node_modules/jest/bin/jest --config config-jest-multiproject.js', + { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + stdio: 'inherit' + } + ) + childProcess.on('exit', () => { + eventsPromise.then(() => { + done() + }).catch(done) + }) + }) } const reportingOptions = ['agentless', 'evp proxy'] @@ -610,9 +646,11 @@ testFrameworks.forEach(({ receiver.setInfoResponse({ endpoints: ['/evp_proxy/v4'] }) } // Tests from ci-visibility/test/ci-visibility-test-2.js will be considered new - receiver.setKnownTests([ - `${name}.ci-visibility/test/ci-visibility-test.js.ci visibility can report tests` - ]) + receiver.setKnownTests({ + [name]: { + 'ci-visibility/test/ci-visibility-test.js': ['ci visibility can report tests'] + } + }) const NUM_RETRIES_EFD = 3 receiver.setSettings({ itr_enabled: false, @@ -692,9 +730,11 @@ testFrameworks.forEach(({ receiver.setInfoResponse({ endpoints: ['/evp_proxy/v4'] }) } // Tests from ci-visibility/test-early-flake-detection/test-parameterized.js will be considered new - receiver.setKnownTests([ - `${name}.ci-visibility/test-early-flake-detection/test.js.ci visibility can report tests` - ]) + receiver.setKnownTests({ + [name]: { + 'ci-visibility/test-early-flake-detection/test.js': ['ci visibility can report tests'] + } + }) receiver.setSettings({ itr_enabled: false, code_coverage: false, @@ -775,9 +815,11 @@ testFrameworks.forEach(({ receiver.setInfoResponse({ endpoints: ['/evp_proxy/v4'] }) } // Tests from ci-visibility/test/ci-visibility-test-2.js will be considered new - receiver.setKnownTests([ - `${name}.ci-visibility/test/ci-visibility-test.js.ci visibility can report tests` - ]) + receiver.setKnownTests({ + [name]: { + 'ci-visibility/test/ci-visibility-test.js': ['ci visibility can report tests'] + } + }) receiver.setSettings({ itr_enabled: false, code_coverage: false, @@ -837,7 +879,7 @@ testFrameworks.forEach(({ receiver.setInfoResponse({ endpoints: ['/evp_proxy/v4'] }) } // Tests from ci-visibility/test/occasionally-failing-test will be considered new - receiver.setKnownTests([]) + receiver.setKnownTests({}) const NUM_RETRIES_EFD = 5 receiver.setSettings({ @@ -909,7 +951,7 @@ testFrameworks.forEach(({ receiver.setInfoResponse({ endpoints: ['/evp_proxy/v4'] }) } // Tests from ci-visibility/test/skipped-and-todo-test will be considered new - receiver.setKnownTests([]) + receiver.setKnownTests({}) const NUM_RETRIES_EFD = 5 receiver.setSettings({ diff --git a/integration-tests/ci-visibility/playwright-tests-error/before-all-timeout-test.js b/integration-tests/ci-visibility/playwright-tests-error/before-all-timeout-test.js new file mode 100644 index 00000000000..9736f2b801d --- /dev/null +++ b/integration-tests/ci-visibility/playwright-tests-error/before-all-timeout-test.js @@ -0,0 +1,19 @@ +const { test, expect } = require('@playwright/test') + +test.beforeEach(async ({ page }) => { + await page.goto(process.env.PW_BASE_URL) +}) + +const waitFor = (ms) => new Promise(resolve => setTimeout(resolve, ms)) + +test.describe('playwright', () => { + test.beforeAll(async () => { + // timeout error + await waitFor(3100) + }) + test('should work with passing tests', async ({ page }) => { + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello World' + ]) + }) +}) diff --git a/integration-tests/config-jest-multiproject.js b/integration-tests/config-jest-multiproject.js new file mode 100644 index 00000000000..e06aec35930 --- /dev/null +++ b/integration-tests/config-jest-multiproject.js @@ -0,0 +1,20 @@ +module.exports = { + projects: [ + { + displayName: 'standard', + testPathIgnorePatterns: ['/node_modules/'], + cache: false, + testMatch: [ + '**/ci-visibility/test/ci-visibility-test*' + ] + }, + { + displayName: 'node', + testPathIgnorePatterns: ['/node_modules/'], + cache: false, + testMatch: [ + '**/ci-visibility/test/ci-visibility-test*' + ] + } + ] +} diff --git a/integration-tests/cucumber/cucumber.spec.js b/integration-tests/cucumber/cucumber.spec.js index d3c80ba6c8f..3c06fd404cc 100644 --- a/integration-tests/cucumber/cucumber.spec.js +++ b/integration-tests/cucumber/cucumber.spec.js @@ -796,13 +796,14 @@ versions.forEach(version => { } }) // "cucumber.ci-visibility/features/farewell.feature.Say" whatever will be considered new - receiver.setKnownTests([ - 'cucumber.ci-visibility/features/farewell.feature.Say farewell', - 'cucumber.ci-visibility/features/greetings.feature.Say greetings', - 'cucumber.ci-visibility/features/greetings.feature.Say yeah', - 'cucumber.ci-visibility/features/greetings.feature.Say yo', - 'cucumber.ci-visibility/features/greetings.feature.Say skip' - ]) + receiver.setKnownTests( + { + cucumber: { + 'ci-visibility/features/farewell.feature': ['Say farewell'], + 'ci-visibility/features/greetings.feature': ['Say greetings', 'Say yeah', 'Say yo', 'Say skip'] + } + } + ) const eventsPromise = receiver .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { const events = payloads.flatMap(({ payload }) => payload.events) @@ -871,13 +872,12 @@ versions.forEach(version => { assert.equal(newTests.length, 0) }) // cucumber.ci-visibility/features/farewell.feature.Say whatever will be considered new - receiver.setKnownTests([ - 'cucumber.ci-visibility/features/farewell.feature.Say farewell', - 'cucumber.ci-visibility/features/greetings.feature.Say greetings', - 'cucumber.ci-visibility/features/greetings.feature.Say yeah', - 'cucumber.ci-visibility/features/greetings.feature.Say yo', - 'cucumber.ci-visibility/features/greetings.feature.Say skip' - ]) + receiver.setKnownTests({ + cucumber: { + 'ci-visibility/features/farewell.feature': ['Say farewell'], + 'ci-visibility/features/greetings.feature': ['Say greetings', 'Say yeah', 'Say yo', 'Say skip'] + } + }) childProcess = exec( runTestsCommand, @@ -907,7 +907,7 @@ versions.forEach(version => { } }) // Tests in "cucumber.ci-visibility/features-flaky/flaky.feature" will be considered new - receiver.setKnownTests([]) + receiver.setKnownTests({}) const eventsPromise = receiver .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { @@ -959,12 +959,12 @@ versions.forEach(version => { }) // "cucumber.ci-visibility/features/farewell.feature.Say whatever" will be considered new // "cucumber.ci-visibility/features/greetings.feature.Say skip" will be considered new - receiver.setKnownTests([ - 'cucumber.ci-visibility/features/farewell.feature.Say farewell', - 'cucumber.ci-visibility/features/greetings.feature.Say greetings', - 'cucumber.ci-visibility/features/greetings.feature.Say yeah', - 'cucumber.ci-visibility/features/greetings.feature.Say yo' - ]) + receiver.setKnownTests({ + cucumber: { + 'ci-visibility/features/farewell.feature': ['Say farewell'], + 'ci-visibility/features/greetings.feature': ['Say greetings', 'Say yeah', 'Say yo'] + } + }) const eventsPromise = receiver .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { diff --git a/integration-tests/cypress-esm-config.mjs b/integration-tests/cypress-esm-config.mjs index 5d97dffd898..f3a01b6f015 100644 --- a/integration-tests/cypress-esm-config.mjs +++ b/integration-tests/cypress-esm-config.mjs @@ -15,7 +15,26 @@ async function runCypress () { import('dd-trace/ci/cypress/plugin').then(module => { module.default(on, config) }) - } + if (process.env.CYPRESS_ENABLE_AFTER_RUN_CUSTOM) { + on('after:run', (...args) => { + // do custom stuff + // and call after-run at the end + return import('dd-trace/ci/cypress/after-run').then(module => { + module.default(...args) + }) + }) + } + if (process.env.CYPRESS_ENABLE_AFTER_SPEC_CUSTOM) { + on('after:spec', (...args) => { + // do custom stuff + // and call after-spec at the end + return import('dd-trace/ci/cypress/after-spec').then(module => { + module.default(...args) + }) + }) + } + }, + specPattern: process.env.SPEC_PATTERN || 'cypress/e2e/**/*.cy.js' }, video: false, screenshotOnRunFailure: false diff --git a/integration-tests/cypress.config.js b/integration-tests/cypress.config.js index 59cdf557048..9d328d9167b 100644 --- a/integration-tests/cypress.config.js +++ b/integration-tests/cypress.config.js @@ -1,19 +1,32 @@ +const ddAfterRun = require('dd-trace/ci/cypress/after-run') +const ddAfterSpec = require('dd-trace/ci/cypress/after-spec') +const cypressFailFast = require('cypress-fail-fast/plugin') +const ddTracePlugin = require('dd-trace/ci/cypress/plugin') + module.exports = { defaultCommandTimeout: 100, e2e: { setupNodeEvents (on, config) { if (process.env.CYPRESS_ENABLE_INCOMPATIBLE_PLUGIN) { - require('cypress-fail-fast/plugin')(on, config) + cypressFailFast(on, config) } - require('dd-trace/ci/cypress/plugin')(on, config) + ddTracePlugin(on, config) if (process.env.CYPRESS_ENABLE_AFTER_RUN_CUSTOM) { on('after:run', (...args) => { // do custom stuff // and call after-run at the end - require('dd-trace/ci/cypress/after-run')(...args) + return ddAfterRun(...args) }) } - } + if (process.env.CYPRESS_ENABLE_AFTER_SPEC_CUSTOM) { + on('after:spec', (...args) => { + // do custom stuff + // and call after-spec at the end + return ddAfterSpec(...args) + }) + } + }, + specPattern: process.env.SPEC_PATTERN || 'cypress/e2e/**/*.cy.js' }, video: false, screenshotOnRunFailure: false diff --git a/integration-tests/cypress/cypress.spec.js b/integration-tests/cypress/cypress.spec.js index a14768b3d42..4a0c993a219 100644 --- a/integration-tests/cypress/cypress.spec.js +++ b/integration-tests/cypress/cypress.spec.js @@ -27,19 +27,23 @@ const { TEST_ITR_SKIPPING_TYPE, TEST_ITR_UNSKIPPABLE, TEST_ITR_FORCED_RUN, - TEST_SOURCE_FILE + TEST_SOURCE_FILE, + TEST_IS_NEW, + TEST_EARLY_FLAKE_IS_RETRY, + TEST_EARLY_FLAKE_IS_ENABLED } = require('../../packages/dd-trace/src/plugins/util/test') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') const { NODE_MAJOR } = require('../../version') const version = process.env.CYPRESS_VERSION const hookFile = 'dd-trace/loader-hook.mjs' +const NUM_RETRIES_EFD = 3 const moduleType = [ { type: 'commonJS', testCommand: function commandWithSuffic (version) { - const commandSuffix = version === '6.7.0' ? '--config-file cypress-config.json' : '' + const commandSuffix = version === '6.7.0' ? '--config-file cypress-config.json --spec "cypress/e2e/*.cy.js"' : '' return `./node_modules/.bin/cypress run ${commandSuffix}` } }, @@ -863,5 +867,236 @@ moduleType.forEach(({ }).catch(done) }) }) + + it('works if after:spec is explicitly used', (done) => { + const receiverPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + const testSessionEvent = events.find(event => event.type === 'test_session_end') + assert.exists(testSessionEvent) + const testModuleEvent = events.find(event => event.type === 'test_module_end') + assert.exists(testModuleEvent) + const testSuiteEvents = events.filter(event => event.type === 'test_suite_end') + assert.equal(testSuiteEvents.length, 4) + const testEvents = events.filter(event => event.type === 'test') + assert.equal(testEvents.length, 9) + }) + + const { + NODE_OPTIONS, // NODE_OPTIONS dd-trace config does not work with cypress + ...restEnvVars + } = getCiVisEvpProxyConfig(receiver.port) + + childProcess = exec( + testCommand, + { + cwd, + env: { + ...restEnvVars, + CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_ENABLE_AFTER_SPEC_CUSTOM: '1' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + receiverPromise.then(() => { + done() + }).catch(done) + }) + }) + + context('early flake detection', () => { + it('retries new tests', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + early_flake_detection: { + enabled: true, + slow_test_retries: { + '5s': NUM_RETRIES_EFD + } + } + }) + + receiver.setKnownTests({ + 'cypress': { + 'cypress/e2e/spec.cy.js': [ + // 'context passes', // This test will be considered new + 'other context fails' + ] + } + }) + + const receiverPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + assert.equal(tests.length, 5) + + const newTests = tests.filter(test => test.meta[TEST_IS_NEW] === 'true') + assert.equal(newTests.length, NUM_RETRIES_EFD + 1) + + const retriedTests = tests.filter(test => test.meta[TEST_EARLY_FLAKE_IS_RETRY] === 'true') + assert.equal(retriedTests.length, NUM_RETRIES_EFD) + + newTests.forEach(newTest => { + assert.equal(newTest.resource, 'cypress/e2e/spec.cy.js.context passes') + }) + + const knownTest = tests.filter(test => !test.meta[TEST_IS_NEW]) + assert.equal(knownTest.length, 1) + assert.equal(knownTest[0].resource, 'cypress/e2e/spec.cy.js.other context fails') + + const testSession = events.find(event => event.type === 'test_session_end').content + assert.propertyVal(testSession.meta, TEST_EARLY_FLAKE_IS_ENABLED, 'true') + }) + + const { + NODE_OPTIONS, // NODE_OPTIONS dd-trace config does not work with cypress + ...restEnvVars + } = getCiVisEvpProxyConfig(receiver.port) + + const specToRun = 'cypress/e2e/spec.cy.js' + + childProcess = exec( + version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, + { + cwd, + env: { + ...restEnvVars, + CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + SPEC_PATTERN: specToRun + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + receiverPromise.then(() => { + done() + }).catch(done) + }) + }) + it('is disabled if DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED is false', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + early_flake_detection: { + enabled: true, + slow_test_retries: { + '5s': NUM_RETRIES_EFD + } + } + }) + + receiver.setKnownTests({ + 'cypress': { + 'cypress/e2e/spec.cy.js': [ + // 'context passes', // This test will be considered new + 'other context fails' + ] + } + }) + + const { + NODE_OPTIONS, // NODE_OPTIONS dd-trace config does not work with cypress + ...restEnvVars + } = getCiVisEvpProxyConfig(receiver.port) + + const receiverPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + assert.equal(tests.length, 2) + + const newTests = tests.filter(test => test.meta[TEST_IS_NEW] === 'true') + assert.equal(newTests.length, 0) + + const testSession = events.find(event => event.type === 'test_session_end').content + assert.notProperty(testSession.meta, TEST_EARLY_FLAKE_IS_ENABLED) + }) + + const specToRun = 'cypress/e2e/spec.cy.js' + childProcess = exec( + version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, + { + cwd, + env: { + ...restEnvVars, + CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + SPEC_PATTERN: specToRun, + DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED: 'false' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + receiverPromise.then(() => { + done() + }).catch(done) + }) + }) + it('does not retry tests that are skipped', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + early_flake_detection: { + enabled: true, + slow_test_retries: { + '5s': NUM_RETRIES_EFD + } + } + }) + + receiver.setKnownTests({}) + const { + NODE_OPTIONS, // NODE_OPTIONS dd-trace config does not work with cypress + ...restEnvVars + } = getCiVisEvpProxyConfig(receiver.port) + + const receiverPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + assert.equal(tests.length, 1) + + const newTests = tests.filter(test => test.meta[TEST_IS_NEW] === 'true') + assert.equal(newTests.length, 0) + + assert.equal(tests[0].resource, 'cypress/e2e/skipped-test.js.skipped skipped') + assert.propertyVal(tests[0].meta, TEST_STATUS, 'skip') + + const testSession = events.find(event => event.type === 'test_session_end').content + assert.propertyVal(testSession.meta, TEST_EARLY_FLAKE_IS_ENABLED, 'true') + }) + + const specToRun = 'cypress/e2e/skipped-test.js' + + childProcess = exec( + version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, + { + cwd, + env: { + ...restEnvVars, + CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + SPEC_PATTERN: 'cypress/e2e/skipped-test.js' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + receiverPromise.then(() => { + done() + }).catch(done) + }) + }) + }) }) }) diff --git a/integration-tests/cypress/e2e/skipped-test.js b/integration-tests/cypress/e2e/skipped-test.js new file mode 100644 index 00000000000..9ffadd09259 --- /dev/null +++ b/integration-tests/cypress/e2e/skipped-test.js @@ -0,0 +1,8 @@ +/* eslint-disable */ +describe('skipped', () => { + it.skip('skipped', () => { + cy.visit('/') + .get('.hello-world') + .should('have.text', 'Hello World') + }) +}) diff --git a/integration-tests/playwright.config.js b/integration-tests/playwright.config.js index 1490ee32fe1..7b57f6b4183 100644 --- a/integration-tests/playwright.config.js +++ b/integration-tests/playwright.config.js @@ -3,7 +3,8 @@ const { devices } = require('@playwright/test') module.exports = { baseURL: process.env.PW_BASE_URL, - testDir: './ci-visibility/playwright-tests', + testDir: process.env.TEST_DIR || './ci-visibility/playwright-tests', + timeout: Number(process.env.TEST_TIMEOUT) || 30000, reporter: 'line', /* Configure projects for major browsers */ projects: [ diff --git a/integration-tests/playwright/playwright.spec.js b/integration-tests/playwright/playwright.spec.js index 0c09a180b9e..a6b27785862 100644 --- a/integration-tests/playwright/playwright.spec.js +++ b/integration-tests/playwright/playwright.spec.js @@ -19,6 +19,7 @@ const { TEST_SOURCE_FILE, TEST_CONFIGURATION_BROWSER_NAME } = require('../../packages/dd-trace/src/plugins/util/test') +const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') const versions = ['1.18.0', 'latest'] @@ -76,6 +77,10 @@ versions.forEach((version) => { assert.equal(testModuleEvent.content.meta[TEST_STATUS], 'fail') assert.equal(testSessionEvent.content.meta[TEST_TYPE], 'browser') assert.equal(testModuleEvent.content.meta[TEST_TYPE], 'browser') + + assert.exists(testSessionEvent.content.meta[ERROR_MESSAGE]) + assert.exists(testModuleEvent.content.meta[ERROR_MESSAGE]) + assert.includeMembers(testSuiteEvents.map(suite => suite.content.resource), [ 'test_suite.todo-list-page-test.js', 'test_suite.landing-page-test.js', @@ -88,6 +93,12 @@ versions.forEach((version) => { 'skip' ]) + testSuiteEvents.forEach(testSuiteEvent => { + if (testSuiteEvent.content.meta[TEST_STATUS] === 'fail') { + assert.exists(testSuiteEvent.content.meta[ERROR_MESSAGE]) + } + }) + assert.includeMembers(testEvents.map(test => test.content.resource), [ 'landing-page-test.js.should work with passing tests', 'landing-page-test.js.should work with skipped tests', @@ -178,5 +189,33 @@ versions.forEach((version) => { testOutput += chunk.toString() }) }) + + it('works when before all fails and step durations are negative', (done) => { + receiver.gatherPayloadsMaxTimeout(({ url }) => url === '/api/v2/citestcycle', payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const testSuiteEvent = events.find(event => event.type === 'test_suite_end').content + const testSessionEvent = events.find(event => event.type === 'test_session_end').content + + assert.propertyVal(testSuiteEvent.meta, TEST_STATUS, 'fail') + assert.propertyVal(testSessionEvent.meta, TEST_STATUS, 'fail') + assert.exists(testSuiteEvent.meta[ERROR_MESSAGE]) + assert.include(testSessionEvent.meta[ERROR_MESSAGE], 'Test suites failed: 1') + }).then(() => done()).catch(done) + + childProcess = exec( + './node_modules/.bin/playwright test -c playwright.config.js', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + PW_BASE_URL: `http://localhost:${webAppPort}`, + TEST_DIR: './ci-visibility/playwright-tests-error', + TEST_TIMEOUT: 3000 + }, + stdio: 'pipe' + } + ) + }) }) }) diff --git a/integration-tests/startup.spec.js b/integration-tests/startup.spec.js index 4033bc65ee9..c0365ef5fee 100644 --- a/integration-tests/startup.spec.js +++ b/integration-tests/startup.spec.js @@ -8,154 +8,179 @@ const { } = require('./helpers') const path = require('path') const { assert } = require('chai') - -describe('startup', () => { - let agent - let proc - let sandbox - let cwd - let startupTestFile - - before(async () => { - sandbox = await createSandbox() - cwd = sandbox.folder - startupTestFile = path.join(cwd, 'startup/index.js') - }) - - after(async () => { - await sandbox.remove() - }) - - context('programmatic', () => { - beforeEach(async () => { - agent = await new FakeAgent().start() +const semver = require('semver') + +const execArgvs = [ + { + execArgv: [] + }, + { + execArgv: ['--import', 'dd-trace/register.js'], + skip: semver.satisfies(process.versions.node, '<20.6') + }, + { + execArgv: ['--loader', 'dd-trace/loader-hook.mjs'], + skip: semver.satisfies(process.versions.node, '>=20.6') + } +] + +execArgvs.forEach(({ execArgv, skip }) => { + const describe = skip ? globalThis.describe.skip : globalThis.describe + + describe(`startup ${execArgv.join(' ')}`, () => { + let agent + let proc + let sandbox + let cwd + let startupTestFile + + before(async () => { + sandbox = await createSandbox() + cwd = sandbox.folder + startupTestFile = path.join(cwd, 'startup/index.js') }) - afterEach(async () => { - proc.kill() - await agent.stop() + after(async () => { + await sandbox.remove() }) - it('works for options.port', async () => { - proc = await spawnProc(startupTestFile, { - cwd, - env: { - AGENT_PORT: agent.port - } - }) - return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { - assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`) - assert.isArray(payload) - assert.strictEqual(payload.length, 1) - assert.isArray(payload[0]) - assert.strictEqual(payload[0].length, 1) - assert.propertyVal(payload[0][0], 'name', 'web.request') + context('programmatic', () => { + beforeEach(async () => { + agent = await new FakeAgent().start() }) - }) - it('works for options.url', async () => { - proc = await spawnProc(startupTestFile, { - cwd, - env: { - AGENT_URL: `http://localhost:${agent.port}` - } - }) - return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { - assert.propertyVal(headers, 'host', `localhost:${agent.port}`) - assert.isArray(payload) - assert.strictEqual(payload.length, 1) - assert.isArray(payload[0]) - assert.strictEqual(payload[0].length, 1) - assert.propertyVal(payload[0][0], 'name', 'web.request') + afterEach(async () => { + proc.kill() + await agent.stop() }) - }) - }) - - context('env var', () => { - beforeEach(async () => { - agent = await new FakeAgent().start() - }) - afterEach(async () => { - proc.kill() - await agent.stop() - }) - - it('works for DD_TRACE_AGENT_PORT', async () => { - proc = await spawnProc(startupTestFile, { - cwd, - env: { - DD_TRACE_AGENT_PORT: agent.port - } + it('works for options.port', async () => { + proc = await spawnProc(startupTestFile, { + cwd, + execArgv, + env: { + AGENT_PORT: agent.port + } + }) + return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { + assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`) + assert.isArray(payload) + assert.strictEqual(payload.length, 1) + assert.isArray(payload[0]) + assert.strictEqual(payload[0].length, 1) + assert.propertyVal(payload[0][0], 'name', 'web.request') + }) }) - return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { - assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`) - assert.isArray(payload) - assert.strictEqual(payload.length, 1) - assert.isArray(payload[0]) - assert.strictEqual(payload[0].length, 1) - assert.propertyVal(payload[0][0], 'name', 'web.request') + + it('works for options.url', async () => { + proc = await spawnProc(startupTestFile, { + cwd, + execArgv, + env: { + AGENT_URL: `http://localhost:${agent.port}` + } + }) + return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { + assert.propertyVal(headers, 'host', `localhost:${agent.port}`) + assert.isArray(payload) + assert.strictEqual(payload.length, 1) + assert.isArray(payload[0]) + assert.strictEqual(payload[0].length, 1) + assert.propertyVal(payload[0][0], 'name', 'web.request') + }) }) }) - it('works for DD_TRACE_AGENT_URL', async () => { - proc = await spawnProc(startupTestFile, { - cwd, - env: { - DD_TRACE_AGENT_URL: `http://localhost:${agent.port}` - } + context('env var', () => { + beforeEach(async () => { + agent = await new FakeAgent().start() }) - return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { - assert.propertyVal(headers, 'host', `localhost:${agent.port}`) - assert.isArray(payload) - assert.strictEqual(payload.length, 1) - assert.isArray(payload[0]) - assert.strictEqual(payload[0].length, 1) - assert.propertyVal(payload[0][0], 'name', 'web.request') + + afterEach(async () => { + proc.kill() + await agent.stop() }) - }) - }) - context('default', () => { - beforeEach(async () => { - // Note that this test will *always* listen on the default port. If that - // port is unavailable, the test will fail. - agent = await new FakeAgent(8126).start() - }) + it('works for DD_TRACE_AGENT_PORT', async () => { + proc = await spawnProc(startupTestFile, { + cwd, + execArgv, + env: { + DD_TRACE_AGENT_PORT: agent.port + } + }) + return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { + assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`) + assert.isArray(payload) + assert.strictEqual(payload.length, 1) + assert.isArray(payload[0]) + assert.strictEqual(payload[0].length, 1) + assert.propertyVal(payload[0][0], 'name', 'web.request') + }) + }) - afterEach(async () => { - proc.kill() - await agent.stop() + it('works for DD_TRACE_AGENT_URL', async () => { + proc = await spawnProc(startupTestFile, { + cwd, + execArgv, + env: { + DD_TRACE_AGENT_URL: `http://localhost:${agent.port}` + } + }) + return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { + assert.propertyVal(headers, 'host', `localhost:${agent.port}`) + assert.isArray(payload) + assert.strictEqual(payload.length, 1) + assert.isArray(payload[0]) + assert.strictEqual(payload[0].length, 1) + assert.propertyVal(payload[0][0], 'name', 'web.request') + }) + }) }) - it('works for hostname and port', async () => { - proc = await spawnProc(startupTestFile, { - cwd + context('default', () => { + beforeEach(async () => { + // Note that this test will *always* listen on the default port. If that + // port is unavailable, the test will fail. + agent = await new FakeAgent(8126).start() }) - return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { - assert.propertyVal(headers, 'host', '127.0.0.1:8126') - assert.isArray(payload) - assert.strictEqual(payload.length, 1) - assert.isArray(payload[0]) - assert.strictEqual(payload[0].length, 1) - assert.propertyVal(payload[0][0], 'name', 'web.request') + + afterEach(async () => { + proc.kill() + await agent.stop() }) - }) - it('works with stealthy-require', async () => { - proc = await spawnProc(startupTestFile, { - cwd, - env: { - STEALTHY_REQUIRE: 'true' - } + it('works for hostname and port', async () => { + proc = await spawnProc(startupTestFile, { + cwd, + execArgv + }) + return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { + assert.propertyVal(headers, 'host', '127.0.0.1:8126') + assert.isArray(payload) + assert.strictEqual(payload.length, 1) + assert.isArray(payload[0]) + assert.strictEqual(payload[0].length, 1) + assert.propertyVal(payload[0][0], 'name', 'web.request') + }) }) - return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { - assert.propertyVal(headers, 'host', '127.0.0.1:8126') - assert.isArray(payload) - assert.strictEqual(payload.length, 1) - assert.isArray(payload[0]) - assert.strictEqual(payload[0].length, 1) - assert.propertyVal(payload[0][0], 'name', 'web.request') + + it('works with stealthy-require', async () => { + proc = await spawnProc(startupTestFile, { + cwd, + execArgv, + env: { + STEALTHY_REQUIRE: 'true' + } + }) + return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { + assert.propertyVal(headers, 'host', '127.0.0.1:8126') + assert.isArray(payload) + assert.strictEqual(payload.length, 1) + assert.isArray(payload[0]) + assert.strictEqual(payload[0].length, 1) + assert.propertyVal(payload[0][0], 'name', 'web.request') + }) }) }) }) diff --git a/package.json b/package.json index df311eeff42..2088cf29cba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dd-trace", - "version": "3.50.0", + "version": "3.51.0", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts", @@ -69,11 +69,11 @@ "node": ">=14" }, "dependencies": { - "@datadog/native-appsec": "7.0.0", + "@datadog/native-appsec": "7.1.0", "@datadog/native-iast-rewriter": "2.2.3", "@datadog/native-iast-taint-tracking": "1.7.0", "@datadog/native-metrics": "^2.0.0", - "@datadog/pprof": "5.0.0", + "@datadog/pprof": "5.1.0", "@datadog/sketches-js": "^2.1.0", "@opentelemetry/api": "^1.0.0", "@opentelemetry/core": "^1.14.0", diff --git a/packages/datadog-core/src/utils/src/get.js b/packages/datadog-core/src/utils/src/get.js new file mode 100644 index 00000000000..f5913c3016c --- /dev/null +++ b/packages/datadog-core/src/utils/src/get.js @@ -0,0 +1,11 @@ +'use strict' + +module.exports = (object, path) => { + const pathArr = path.split('.') + let val = object + for (const p of pathArr) { + if (val === undefined) return val + val = val[p] + } + return val +} diff --git a/packages/datadog-core/src/utils/src/has.js b/packages/datadog-core/src/utils/src/has.js new file mode 100644 index 00000000000..fafc7cc775f --- /dev/null +++ b/packages/datadog-core/src/utils/src/has.js @@ -0,0 +1,14 @@ +'use strict' + +module.exports = (object, path) => { + const pathArr = path.split('.') + let property = object + for (const n of pathArr) { + if (property.hasOwnProperty(n)) { + property = property[n] + } else { + return false + } + } + return true +} diff --git a/packages/utils/src/kebabcase.js b/packages/datadog-core/src/utils/src/kebabcase.js similarity index 100% rename from packages/utils/src/kebabcase.js rename to packages/datadog-core/src/utils/src/kebabcase.js diff --git a/packages/utils/src/pick.js b/packages/datadog-core/src/utils/src/pick.js similarity index 100% rename from packages/utils/src/pick.js rename to packages/datadog-core/src/utils/src/pick.js diff --git a/packages/datadog-core/src/utils/src/set.js b/packages/datadog-core/src/utils/src/set.js new file mode 100644 index 00000000000..e6b9fc7f12a --- /dev/null +++ b/packages/datadog-core/src/utils/src/set.js @@ -0,0 +1,16 @@ +'use strict' + +module.exports = (object, path, value) => { + const pathArr = path.split('.') + let property = object + let i + for (i = 0; i < pathArr.length - 1; i++) { + const n = pathArr[i] + if (property.hasOwnProperty(n)) { + property = property[n] + } else { + property[n] = property = {} + } + } + property[pathArr[i]] = value +} diff --git a/packages/utils/src/uniq.js b/packages/datadog-core/src/utils/src/uniq.js similarity index 100% rename from packages/utils/src/uniq.js rename to packages/datadog-core/src/utils/src/uniq.js diff --git a/packages/datadog-core/test/utils/src/get.spec.js b/packages/datadog-core/test/utils/src/get.spec.js new file mode 100644 index 00000000000..4815b9bf3ee --- /dev/null +++ b/packages/datadog-core/test/utils/src/get.spec.js @@ -0,0 +1,22 @@ +'use strict' + +require('../../../../dd-trace/test/setup/tap') + +const { expect } = require('chai') +const get = require('../../../src/utils/src/get') + +describe('get', () => { + const obj = { + 'a': { + 'b': 'c' + } + } + + it('should return value at path', () => { + expect(get(obj, 'a.b')).to.be.equal('c') + }) + + it('should return undefined if path does not exist', () => { + expect(get(obj, 'd')).to.be.undefined + }) +}) diff --git a/packages/datadog-core/test/utils/src/has.spec.js b/packages/datadog-core/test/utils/src/has.spec.js new file mode 100644 index 00000000000..54130547ceb --- /dev/null +++ b/packages/datadog-core/test/utils/src/has.spec.js @@ -0,0 +1,22 @@ +'use strict' + +require('../../../../dd-trace/test/setup/tap') + +const { expect } = require('chai') +const has = require('../../../src/utils/src/has') + +describe('has', () => { + const obj = { + 'a': { + 'b': 'c' + } + } + + it('should true if path exists', () => { + expect(has(obj, 'a.b')).to.be.true + }) + + it('should return false if path does not exist', () => { + expect(has(obj, 'd')).to.be.false + }) +}) diff --git a/packages/datadog-core/test/utils/src/set.spec.js b/packages/datadog-core/test/utils/src/set.spec.js new file mode 100644 index 00000000000..02f9c7c19d7 --- /dev/null +++ b/packages/datadog-core/test/utils/src/set.spec.js @@ -0,0 +1,15 @@ +'use strict' + +require('../../../../dd-trace/test/setup/tap') + +const { expect } = require('chai') +const set = require('../../../src/utils/src/set') + +describe('set', () => { + const obj = {} + + it('should set value at path', () => { + set(obj, 'a.b', 'c') + expect(obj.a.b).to.be.equal('c') + }) +}) diff --git a/packages/datadog-instrumentations/src/amqplib.js b/packages/datadog-instrumentations/src/amqplib.js index 2e6835aad8d..a5d07ebb4ab 100644 --- a/packages/datadog-instrumentations/src/amqplib.js +++ b/packages/datadog-instrumentations/src/amqplib.js @@ -5,7 +5,7 @@ const { addHook, AsyncResource } = require('./helpers/instrument') -const kebabCase = require('../../utils/src/kebabcase') +const kebabCase = require('../../datadog-core/src/utils/src/kebabcase') const shimmer = require('../../datadog-shimmer') const startCh = channel('apm:amqplib:command:start') diff --git a/packages/datadog-instrumentations/src/cucumber.js b/packages/datadog-instrumentations/src/cucumber.js index 385eaa9e39f..89d4895220c 100644 --- a/packages/datadog-instrumentations/src/cucumber.js +++ b/packages/datadog-instrumentations/src/cucumber.js @@ -92,7 +92,8 @@ function getStatusFromResultLatest (result) { } function isNewTest (testSuite, testName) { - return !knownTests.includes(`cucumber.${testSuite}.${testName}`) + const testsForSuite = knownTests.cucumber?.[testSuite] || [] + return !testsForSuite.includes(testName) } function getTestStatusFromRetries (testStatuses) { diff --git a/packages/datadog-instrumentations/src/grpc/server.js b/packages/datadog-instrumentations/src/grpc/server.js index f3fc2c5a1a8..a62af9a30c1 100644 --- a/packages/datadog-instrumentations/src/grpc/server.js +++ b/packages/datadog-instrumentations/src/grpc/server.js @@ -92,7 +92,9 @@ function createWrapEmit (call, ctx, onCancel) { finishChannel.publish(ctx) call.removeListener('cancelled', onCancel) break - case 'finish': + // Streams are always cancelled before `finish` since 1.10.0 so we have + // to use `prefinish` instead to avoid cancellation false positives. + case 'prefinish': if (call.status) { updateChannel.publish(call.status) } diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index 01efb84ce4f..dc1ab7df461 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -115,6 +115,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { this.nameToParams = {} this.global._ddtrace = global._ddtrace + this.displayName = config.projectConfig?.displayName?.name this.testEnvironmentOptions = getTestEnvironmentOptions(config) const repositoryRoot = this.testEnvironmentOptions._ddRepositoryRoot @@ -139,14 +140,16 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { // Function that receives a list of known tests for a test service and // returns the ones that belong to the current suite getKnownTestsForSuite (knownTests) { + if (this.knownTestsForThisSuite) { + return this.knownTestsForThisSuite + } let knownTestsForSuite = knownTests - // If jest runs in band, the known tests are not serialized, so they're an array. - if (!Array.isArray(knownTests)) { + // If jest is using workers, known tests are serialized to json. + // If jest runs in band, they are not. + if (typeof knownTestsForSuite === 'string') { knownTestsForSuite = JSON.parse(knownTestsForSuite) } - return knownTestsForSuite - .filter(test => test.includes(this.testSuite)) - .map(test => test.replace(`jest.${this.testSuite}.`, '').trim()) + return knownTestsForSuite.jest?.[this.testSuite] || [] } // Add the `add_test` event we don't have the test object yet, so @@ -201,6 +204,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { suite: this.testSuite, testSourceFile: this.testSourceFile, runner: 'jest-circus', + displayName: this.displayName, testParameters, frameworkVersion: jestVersion, isNew: isNewTest, @@ -252,6 +256,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { suite: this.testSuite, testSourceFile: this.testSourceFile, runner: 'jest-circus', + displayName: this.displayName, frameworkVersion: jestVersion, testStartLine: getTestLineStart(event.test.asyncError, this.testSuite) }) @@ -559,6 +564,7 @@ function jestAdapterWrapper (jestAdapter, jestVersion) { testSuiteStartCh.publish({ testSuite: environment.testSuite, testEnvironmentOptions: environment.testEnvironmentOptions, + displayName: environment.displayName, frameworkVersion: jestVersion }) return adapter.apply(this, arguments).then(suiteResults => { diff --git a/packages/datadog-instrumentations/src/mocha.js b/packages/datadog-instrumentations/src/mocha.js index a67cb669f0c..4e45d0624ce 100644 --- a/packages/datadog-instrumentations/src/mocha.js +++ b/packages/datadog-instrumentations/src/mocha.js @@ -106,7 +106,10 @@ function getTestFullName (test) { } function isNewTest (test) { - return !knownTests.includes(getTestFullName(test)) + const testSuite = getTestSuitePath(test.file, process.cwd()) + const testName = removeEfdStringFromTestName(test.fullTitle()) + const testsForSuite = knownTests.mocha?.[testSuite] || [] + return !testsForSuite.includes(testName) } function retryTest (test) { diff --git a/packages/datadog-instrumentations/src/mongodb-core.js b/packages/datadog-instrumentations/src/mongodb-core.js index 1e8be322189..1a7309fc24f 100644 --- a/packages/datadog-instrumentations/src/mongodb-core.js +++ b/packages/datadog-instrumentations/src/mongodb-core.js @@ -32,12 +32,18 @@ addHook({ name: 'mongodb', versions: ['>=4 <4.6.0'], file: 'lib/cmap/connection. return Connection }) -addHook({ name: 'mongodb', versions: ['>=4.6.0'], file: 'lib/cmap/connection.js' }, Connection => { +addHook({ name: 'mongodb', versions: ['>=4.6.0 <6.4.0'], file: 'lib/cmap/connection.js' }, Connection => { const proto = Connection.Connection.prototype shimmer.wrap(proto, 'command', command => wrapConnectionCommand(command, 'command')) return Connection }) +addHook({ name: 'mongodb', versions: ['>=6.4.0'], file: 'lib/cmap/connection.js' }, Connection => { + const proto = Connection.Connection.prototype + shimmer.wrap(proto, 'command', command => wrapConnectionCommand(command, 'command', undefined, instrumentPromise)) + return Connection +}) + addHook({ name: 'mongodb', versions: ['>=3.3 <4'], file: 'lib/core/wireprotocol/index.js' }, wp => wrapWp(wp)) addHook({ name: 'mongodb-core', versions: ['>=3.2'], file: 'lib/wireprotocol/index.js' }, wp => wrapWp(wp)) @@ -89,7 +95,7 @@ function wrapUnifiedCommand (command, operation, name) { return shimmer.wrap(command, wrapped) } -function wrapConnectionCommand (command, operation, name) { +function wrapConnectionCommand (command, operation, name, instrumentFn = instrument) { const wrapped = function (ns, ops) { if (!startCh.hasSubscribers) { return command.apply(this, arguments) @@ -101,7 +107,7 @@ function wrapConnectionCommand (command, operation, name) { const topology = { s: { options } } ns = `${ns.db}.${ns.collection}` - return instrument(operation, command, this, arguments, topology, ns, ops, { name }) + return instrumentFn(operation, command, this, arguments, topology, ns, ops, { name }) } return shimmer.wrap(command, wrapped) } @@ -179,3 +185,28 @@ function instrument (operation, command, ctx, args, server, ns, ops, options = { } }) } + +function instrumentPromise (operation, command, ctx, args, server, ns, ops, options = {}) { + const name = options.name || (ops && Object.keys(ops)[0]) + + const serverInfo = server && server.s && server.s.options + const asyncResource = new AsyncResource('bound-anonymous-fn') + + return asyncResource.runInAsyncScope(() => { + startCh.publish({ ns, ops, options: serverInfo, name }) + + const promise = command.apply(ctx, args) + + promise.then(function (res) { + finishCh.publish() + return res + }, function (err) { + errorCh.publish(err) + finishCh.publish() + + return Promise.reject(err) + }) + + return promise + }) +} diff --git a/packages/datadog-instrumentations/src/playwright.js b/packages/datadog-instrumentations/src/playwright.js index 4093faaf6de..42e58e1ca53 100644 --- a/packages/datadog-instrumentations/src/playwright.js +++ b/packages/datadog-instrumentations/src/playwright.js @@ -14,6 +14,7 @@ const testSuiteFinishCh = channel('ci:playwright:test-suite:finish') const testToAr = new WeakMap() const testSuiteToAr = new Map() const testSuiteToTestStatuses = new Map() +const testSuiteToErrors = new Map() let startedSuites = [] @@ -81,7 +82,12 @@ function getRootDir (playwrightRunner) { function getProjectsFromRunner (runner) { const config = getPlaywrightConfig(runner) - return config.projects?.map(({ project }) => project) + return config.projects?.map((project) => { + if (project.project) { + return project.project + } + return project + }) } function getProjectsFromDispatcher (dispatcher) { @@ -93,13 +99,55 @@ function getProjectsFromDispatcher (dispatcher) { return dispatcher._loader?.fullConfig()?.projects } -function getBrowserNameFromProjects (projects, projectId) { - if (!projects) { +function getBrowserNameFromProjects (projects, test) { + if (!projects || !test) { return null } - return projects.find(project => - project.__projectId === projectId || project._id === projectId - )?.name + const { _projectIndex, _projectId: testProjectId } = test + + if (_projectIndex !== undefined) { + return projects[_projectIndex]?.name + } + + return projects.find(({ __projectId, _id, name }) => { + if (__projectId !== undefined) { + return __projectId === testProjectId + } + if (_id !== undefined) { + return _id === testProjectId + } + return name === testProjectId + })?.name +} + +function formatTestHookError (error, hookType, isTimeout) { + let hookError = error + if (error) { + hookError.message = `Error in ${hookType} hook: ${error.message}` + } + if (!hookError && isTimeout) { + hookError = new Error(`${hookType} hook timed out`) + } + return hookError +} + +function addErrorToTestSuite (testSuiteAbsolutePath, error) { + if (testSuiteToErrors.has(testSuiteAbsolutePath)) { + testSuiteToErrors.get(testSuiteAbsolutePath).push(error) + } else { + testSuiteToErrors.set(testSuiteAbsolutePath, [error]) + } +} + +function getTestSuiteError (testSuiteAbsolutePath) { + const errors = testSuiteToErrors.get(testSuiteAbsolutePath) + if (!errors) { + return null + } + if (errors.length === 1) { + return errors[0] + } + return new Error(`${errors.length} errors in this test suite:\n${errors.map(e => e.message).join('\n------\n')}`) } function testBeginHandler (test, browserName) { @@ -131,7 +179,7 @@ function testBeginHandler (test, browserName) { }) } -function testEndHandler (test, annotations, testStatus, error) { +function testEndHandler (test, annotations, testStatus, error, isTimeout) { let annotationTags if (annotations.length) { annotationTags = parseAnnotations(annotations) @@ -139,6 +187,11 @@ function testEndHandler (test, annotations, testStatus, error) { const { _requireFile: testSuiteAbsolutePath, results, _type } = test if (_type === 'beforeAll' || _type === 'afterAll') { + const hookError = formatTestHookError(error, _type, isTimeout) + + if (hookError) { + addErrorToTestSuite(testSuiteAbsolutePath, hookError) + } return } @@ -148,15 +201,20 @@ function testEndHandler (test, annotations, testStatus, error) { testFinishCh.publish({ testStatus, steps: testResult.steps, error, extraTags: annotationTags }) }) - if (!testSuiteToTestStatuses.has(testSuiteAbsolutePath)) { - testSuiteToTestStatuses.set(testSuiteAbsolutePath, [testStatus]) - } else { + if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) { testSuiteToTestStatuses.get(testSuiteAbsolutePath).push(testStatus) + } else { + testSuiteToTestStatuses.set(testSuiteAbsolutePath, [testStatus]) + } + + if (error) { + addErrorToTestSuite(testSuiteAbsolutePath, error) } remainingTestsByFile[testSuiteAbsolutePath] = remainingTestsByFile[testSuiteAbsolutePath] .filter(currentTest => currentTest !== test) + // Last test, we finish the suite if (!remainingTestsByFile[testSuiteAbsolutePath].length) { const testStatuses = testSuiteToTestStatuses.get(testSuiteAbsolutePath) @@ -167,9 +225,10 @@ function testEndHandler (test, annotations, testStatus, error) { testSuiteStatus = 'skip' } + const suiteError = getTestSuiteError(testSuiteAbsolutePath) const testSuiteAsyncResource = testSuiteToAr.get(testSuiteAbsolutePath) testSuiteAsyncResource.runInAsyncScope(() => { - testSuiteFinishCh.publish(testSuiteStatus) + testSuiteFinishCh.publish({ status: testSuiteStatus, error: suiteError }) }) } } @@ -197,7 +256,7 @@ function dispatcherHook (dispatcherExport) { if (method === 'testBegin') { const { test } = dispatcher._testById.get(params.testId) const projects = getProjectsFromDispatcher(dispatcher) - const browser = getBrowserNameFromProjects(projects, test._projectId) + const browser = getBrowserNameFromProjects(projects, test) testBeginHandler(test, browser) } else if (method === 'testEnd') { const { test } = dispatcher._testById.get(params.testId) @@ -205,7 +264,8 @@ function dispatcherHook (dispatcherExport) { const { results } = test const testResult = results[results.length - 1] - testEndHandler(test, params.annotations, STATUS_TO_TEST_STATUS[testResult.status], testResult.error) + const isTimeout = testResult.status === 'timedOut' + testEndHandler(test, params.annotations, STATUS_TO_TEST_STATUS[testResult.status], testResult.error, isTimeout) } }) @@ -232,13 +292,14 @@ function dispatcherHookNew (dispatcherExport, runWrapper) { worker.on('testBegin', ({ testId }) => { const test = getTestByTestId(dispatcher, testId) const projects = getProjectsFromDispatcher(dispatcher) - const browser = getBrowserNameFromProjects(projects, test._projectId) + const browser = getBrowserNameFromProjects(projects, test) testBeginHandler(test, browser) }) worker.on('testEnd', ({ testId, status, errors, annotations }) => { const test = getTestByTestId(dispatcher, testId) - testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0]) + const isTimeout = status === 'timedOut' + testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0], isTimeout) }) return worker @@ -265,7 +326,7 @@ function runnerHook (runnerExport, playwrightVersion) { // there were tests that did not go through `testBegin` or `testEnd`, // because they were skipped tests.forEach(test => { - const browser = getBrowserNameFromProjects(projects, test._projectId) + const browser = getBrowserNameFromProjects(projects, test) testBeginHandler(test, browser) testEndHandler(test, [], 'skip') }) @@ -327,6 +388,7 @@ addHook({ file: 'lib/runner/runner.js', versions: ['>=1.38.0'] }, runnerHook) + addHook({ name: 'playwright', file: 'lib/runner/dispatcher.js', diff --git a/packages/datadog-plugin-amqplib/src/consumer.js b/packages/datadog-plugin-amqplib/src/consumer.js index 582fd313ae3..da4efb33fd0 100644 --- a/packages/datadog-plugin-amqplib/src/consumer.js +++ b/packages/datadog-plugin-amqplib/src/consumer.js @@ -2,7 +2,8 @@ const { TEXT_MAP } = require('../../../ext/formats') const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer') -const { getAmqpMessageSize, CONTEXT_PROPAGATION_KEY } = require('../../dd-trace/src/datastreams/processor') +const { getAmqpMessageSize } = require('../../dd-trace/src/datastreams/processor') +const { DsmPathwayCodec } = require('../../dd-trace/src/datastreams/pathway') const { getResourceName } = require('./util') class AmqplibConsumerPlugin extends ConsumerPlugin { @@ -29,12 +30,12 @@ class AmqplibConsumerPlugin extends ConsumerPlugin { }) if ( - this.config.dsmEnabled && - message?.properties?.headers?.[CONTEXT_PROPAGATION_KEY] + this.config.dsmEnabled && message?.properties?.headers && + DsmPathwayCodec.contextExists(message.properties.headers) ) { const payloadSize = getAmqpMessageSize({ headers: message.properties.headers, content: message.content }) const queue = fields.queue ? fields.queue : fields.routingKey - this.tracer.decodeDataStreamsContext(message.properties.headers[CONTEXT_PROPAGATION_KEY]) + this.tracer.decodeDataStreamsContext(message.properties.headers) this.tracer .setCheckpoint(['direction:in', `topic:${queue}`, 'type:rabbitmq'], span, payloadSize) } diff --git a/packages/datadog-plugin-amqplib/src/producer.js b/packages/datadog-plugin-amqplib/src/producer.js index a07582e50c4..5f299c80a45 100644 --- a/packages/datadog-plugin-amqplib/src/producer.js +++ b/packages/datadog-plugin-amqplib/src/producer.js @@ -3,8 +3,8 @@ const { TEXT_MAP } = require('../../../ext/formats') const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants') const ProducerPlugin = require('../../dd-trace/src/plugins/producer') -const { encodePathwayContext } = require('../../dd-trace/src/datastreams/pathway') -const { getAmqpMessageSize, CONTEXT_PROPAGATION_KEY } = require('../../dd-trace/src/datastreams/processor') +const { DsmPathwayCodec } = require('../../dd-trace/src/datastreams/pathway') +const { getAmqpMessageSize } = require('../../dd-trace/src/datastreams/processor') const { getResourceName } = require('./util') class AmqplibProducerPlugin extends ProducerPlugin { @@ -40,8 +40,7 @@ class AmqplibProducerPlugin extends ProducerPlugin { .setCheckpoint( ['direction:out', `exchange:${fields.exchange}`, `has_routing_key:${hasRoutingKey}`, 'type:rabbitmq'] , span, payloadSize) - const pathwayCtx = encodePathwayContext(dataStreamsContext) - fields.headers[CONTEXT_PROPAGATION_KEY] = pathwayCtx + DsmPathwayCodec.encode(dataStreamsContext, fields.headers) } } } diff --git a/packages/datadog-plugin-aws-sdk/src/base.js b/packages/datadog-plugin-aws-sdk/src/base.js index cb6cf2b6126..2e686cdc9f8 100644 --- a/packages/datadog-plugin-aws-sdk/src/base.js +++ b/packages/datadog-plugin-aws-sdk/src/base.js @@ -126,8 +126,9 @@ class BaseAwsSdkPlugin extends ClientPlugin { if (err) { span.setTag('error', err) - if (err.requestId) { - span.addTags({ 'aws.response.request_id': err.requestId }) + const requestId = err.RequestId || err.requestId + if (requestId) { + span.addTags({ 'aws.response.request_id': requestId }) } } diff --git a/packages/datadog-plugin-aws-sdk/src/services/kinesis.js b/packages/datadog-plugin-aws-sdk/src/services/kinesis.js index 2cecdf646dc..7fc9873c7ac 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +++ b/packages/datadog-plugin-aws-sdk/src/services/kinesis.js @@ -1,9 +1,8 @@ 'use strict' const { - CONTEXT_PROPAGATION_KEY, getSizeOrZero } = require('../../../dd-trace/src/datastreams/processor') -const { encodePathwayContext } = require('../../../dd-trace/src/datastreams/pathway') +const { DsmPathwayCodec } = require('../../../dd-trace/src/datastreams/pathway') const log = require('../../../dd-trace/src/log') const BaseAwsSdkPlugin = require('../base') const { storage } = require('../../../datadog-core') @@ -113,13 +112,10 @@ class Kinesis extends BaseAwsSdkPlugin { const parsedAttributes = JSON.parse(Buffer.from(record.Data).toString()) if ( - parsedAttributes && - parsedAttributes._datadog && - parsedAttributes._datadog[CONTEXT_PROPAGATION_KEY] && - streamName + parsedAttributes?._datadog && streamName && DsmPathwayCodec.contextExists(parsedAttributes._datadog) ) { const payloadSize = getSizeOrZero(record.Data) - this.tracer.decodeDataStreamsContext(Buffer.from(parsedAttributes._datadog[CONTEXT_PROPAGATION_KEY])) + this.tracer.decodeDataStreamsContext(parsedAttributes._datadog) this.tracer .setCheckpoint(['direction:in', `topic:${streamName}`, 'type:kinesis'], span, payloadSize) } @@ -182,10 +178,7 @@ class Kinesis extends BaseAwsSdkPlugin { if (this.config.dsmEnabled) { parsedData._datadog = ddInfo const dataStreamsContext = this.setDSMCheckpoint(span, parsedData, stream) - if (dataStreamsContext) { - const pathwayCtx = encodePathwayContext(dataStreamsContext) - ddInfo[CONTEXT_PROPAGATION_KEY] = pathwayCtx.toJSON() - } + DsmPathwayCodec.encode(dataStreamsContext, ddInfo) } if (Object.keys(ddInfo).length !== 0) { diff --git a/packages/datadog-plugin-aws-sdk/src/services/sns.js b/packages/datadog-plugin-aws-sdk/src/services/sns.js index b6db4cc5fdc..5d1d3b8c708 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/sns.js +++ b/packages/datadog-plugin-aws-sdk/src/services/sns.js @@ -1,6 +1,6 @@ 'use strict' -const { CONTEXT_PROPAGATION_KEY, getHeadersSize } = require('../../../dd-trace/src/datastreams/processor') -const { encodePathwayContext } = require('../../../dd-trace/src/datastreams/pathway') +const { getHeadersSize } = require('../../../dd-trace/src/datastreams/processor') +const { DsmPathwayCodec } = require('../../../dd-trace/src/datastreams/pathway') const log = require('../../../dd-trace/src/log') const BaseAwsSdkPlugin = require('../base') @@ -94,10 +94,7 @@ class Sns extends BaseAwsSdkPlugin { } const dataStreamsContext = this.setDSMCheckpoint(span, params, topicArn) - if (dataStreamsContext) { - const pathwayCtx = encodePathwayContext(dataStreamsContext) - ddInfo[CONTEXT_PROPAGATION_KEY] = pathwayCtx.toJSON() - } + DsmPathwayCodec.encode(dataStreamsContext, ddInfo) } if (Object.keys(ddInfo).length !== 0) { diff --git a/packages/datadog-plugin-aws-sdk/src/services/sqs.js b/packages/datadog-plugin-aws-sdk/src/services/sqs.js index 2c10d081070..556dd488158 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/sqs.js +++ b/packages/datadog-plugin-aws-sdk/src/services/sqs.js @@ -3,8 +3,8 @@ const log = require('../../../dd-trace/src/log') const BaseAwsSdkPlugin = require('../base') const { storage } = require('../../../datadog-core') -const { CONTEXT_PROPAGATION_KEY, getHeadersSize } = require('../../../dd-trace/src/datastreams/processor') -const { encodePathwayContext } = require('../../../dd-trace/src/datastreams/pathway') +const { getHeadersSize } = require('../../../dd-trace/src/datastreams/processor') +const { DsmPathwayCodec } = require('../../../dd-trace/src/datastreams/pathway') class Sqs extends BaseAwsSdkPlugin { static get id () { return 'sqs' } @@ -192,13 +192,13 @@ class Sqs extends BaseAwsSdkPlugin { parsedAttributes = this.parseDatadogAttributes(message.MessageAttributes._datadog) } } - if (parsedAttributes && parsedAttributes[CONTEXT_PROPAGATION_KEY]) { + if (parsedAttributes && DsmPathwayCodec.contextExists(parsedAttributes)) { const payloadSize = getHeadersSize({ Body: message.Body, MessageAttributes: message.MessageAttributes }) const queue = params.QueueUrl.split('/').pop() - this.tracer.decodeDataStreamsContext(Buffer.from(parsedAttributes[CONTEXT_PROPAGATION_KEY])) + this.tracer.decodeDataStreamsContext(parsedAttributes) this.tracer .setCheckpoint(['direction:in', `topic:${queue}`, 'type:sqs'], span, payloadSize) } @@ -252,9 +252,7 @@ class Sqs extends BaseAwsSdkPlugin { const dataStreamsContext = this.setDSMCheckpoint(span, params, queueUrl) if (dataStreamsContext) { - const pathwayCtx = encodePathwayContext(dataStreamsContext) - ddInfo[CONTEXT_PROPAGATION_KEY] = pathwayCtx.toJSON() - + DsmPathwayCodec.encode(dataStreamsContext, ddInfo) params.MessageAttributes._datadog.StringValue = JSON.stringify(ddInfo) } } diff --git a/packages/datadog-plugin-aws-sdk/test/aws-sdk.spec.js b/packages/datadog-plugin-aws-sdk/test/aws-sdk.spec.js index aeb5d5b81fd..c7695ecd48c 100644 --- a/packages/datadog-plugin-aws-sdk/test/aws-sdk.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/aws-sdk.spec.js @@ -122,7 +122,7 @@ describe('Plugin', () => { expect(span).to.include({ name: 'aws.request', - resource: 'completeMultipartUpload', + resource: 'completeMultipartUpload my-bucket', service: 'test-aws-s3' }) @@ -132,9 +132,16 @@ describe('Plugin', () => { [ERROR_STACK]: error.stack, 'component': 'aws-sdk' }) + if (semver.intersects(version, '>=2.3.4')) { + expect(span.meta['aws.response.request_id']).to.match(/[\w]{8}(-[\w]{4}){3}-[\w]{12}/) + } }).then(done, done) - s3.completeMultipartUpload('invalid', e => { + s3.completeMultipartUpload({ + Bucket: 'my-bucket', + Key: 'my-key', + UploadId: 'my-upload-id' + }, e => { error = e }) }) diff --git a/packages/datadog-plugin-cypress/src/after-spec.js b/packages/datadog-plugin-cypress/src/after-spec.js new file mode 100644 index 00000000000..4fdf98ad582 --- /dev/null +++ b/packages/datadog-plugin-cypress/src/after-spec.js @@ -0,0 +1,3 @@ +const cypressPlugin = require('./cypress-plugin') + +module.exports = cypressPlugin.afterSpec.bind(cypressPlugin) diff --git a/packages/datadog-plugin-cypress/src/cypress-plugin.js b/packages/datadog-plugin-cypress/src/cypress-plugin.js index 5c380e53839..204670ec46a 100644 --- a/packages/datadog-plugin-cypress/src/cypress-plugin.js +++ b/packages/datadog-plugin-cypress/src/cypress-plugin.js @@ -25,7 +25,10 @@ const { TEST_ITR_UNSKIPPABLE, TEST_ITR_FORCED_RUN, ITR_CORRELATION_ID, - TEST_SOURCE_FILE + TEST_SOURCE_FILE, + TEST_IS_NEW, + TEST_EARLY_FLAKE_IS_RETRY, + TEST_EARLY_FLAKE_IS_ENABLED } = require('../../dd-trace/src/plugins/util/test') const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util') const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants') @@ -113,12 +116,9 @@ function getLibraryConfiguration (tracer, testConfiguration) { }) } -function getSkippableTests (isSuitesSkippingEnabled, tracer, testConfiguration) { - if (!isSuitesSkippingEnabled) { - return Promise.resolve({ skippableTests: [] }) - } +function getSkippableTests (tracer, testConfiguration) { return new Promise(resolve => { - if (!tracer._tracer._exporter?.getLibraryConfiguration) { + if (!tracer._tracer._exporter?.getSkippableSuites) { return resolve({ err: new Error('CI Visibility was not initialized correctly') }) } tracer._tracer._exporter.getSkippableSuites(testConfiguration, (err, skippableTests, correlationId) => { @@ -131,6 +131,20 @@ function getSkippableTests (isSuitesSkippingEnabled, tracer, testConfiguration) }) } +function getKnownTests (tracer, testConfiguration) { + return new Promise(resolve => { + if (!tracer._tracer._exporter?.getKnownTests) { + return resolve({ err: new Error('CI Visibility was not initialized correctly') }) + } + tracer._tracer._exporter.getKnownTests(testConfiguration, (err, knownTests) => { + resolve({ + err, + knownTests + }) + }) + }) +} + function getSuiteStatus (suiteStats) { if (!suiteStats) { return 'skip' @@ -183,10 +197,14 @@ class CypressPlugin { this.isTestsSkipped = false this.isSuitesSkippingEnabled = false this.isCodeCoverageEnabled = false + this.isEarlyFlakeDetectionEnabled = false + this.earlyFlakeDetectionNumRetries = 0 + this.testsToSkip = [] this.skippedTests = [] this.hasForcedToRunSuites = false this.hasUnskippableSuites = false this.unskippableSuites = [] + this.knownTests = [] } init (tracer, cypressConfig) { @@ -274,66 +292,101 @@ class CypressPlugin { }) } - beforeRun (details) { + isNewTest (testName, testSuite) { + return !this.knownTestsByTestSuite?.[testSuite]?.includes(testName) + } + + async beforeRun (details) { this.command = getCypressCommand(details) this.frameworkVersion = getCypressVersion(details) this.rootDir = getRootDir(details) - return getLibraryConfiguration(this.tracer, this.testConfiguration).then(({ err, libraryConfig }) => { - if (err) { - log.error(err) + const libraryConfigurationResponse = await getLibraryConfiguration(this.tracer, this.testConfiguration) + + if (libraryConfigurationResponse.err) { + log.error(libraryConfigurationResponse.err) + } else { + const { + libraryConfig: { + isSuitesSkippingEnabled, + isCodeCoverageEnabled, + isEarlyFlakeDetectionEnabled, + earlyFlakeDetectionNumRetries + } + } = libraryConfigurationResponse + this.isSuitesSkippingEnabled = isSuitesSkippingEnabled + this.isCodeCoverageEnabled = isCodeCoverageEnabled + this.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled + this.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries + } + + if (this.isEarlyFlakeDetectionEnabled) { + const knownTestsResponse = await getKnownTests( + this.tracer, + this.testConfiguration + ) + if (knownTestsResponse.err) { + log.error(knownTestsResponse.err) } else { - this.isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled - this.isCodeCoverageEnabled = libraryConfig.isCodeCoverageEnabled + // We use TEST_FRAMEWORK_NAME for the name of the module + this.knownTestsByTestSuite = knownTestsResponse.knownTests[TEST_FRAMEWORK_NAME] } + } - return getSkippableTests(this.isSuitesSkippingEnabled, this.tracer, this.testConfiguration) - .then(({ err, skippableTests, correlationId }) => { - if (err) { - log.error(err) - } else { - this.testsToSkip = skippableTests || [] - this.itrCorrelationId = correlationId - } + if (this.isSuitesSkippingEnabled) { + const skippableTestsResponse = await getSkippableTests( + this.tracer, + this.testConfiguration + ) + if (skippableTestsResponse.err) { + log.error(skippableTestsResponse.err) + } else { + const { skippableTests, correlationId } = skippableTestsResponse + this.testsToSkip = skippableTests || [] + this.itrCorrelationId = correlationId + } + } - // `details.specs` are test files - details.specs?.forEach(({ absolute, relative }) => { - const isUnskippableSuite = isMarkedAsUnskippable({ path: absolute }) - if (isUnskippableSuite) { - this.unskippableSuites.push(relative) - } - }) + // `details.specs` are test files + details.specs?.forEach(({ absolute, relative }) => { + const isUnskippableSuite = isMarkedAsUnskippable({ path: absolute }) + if (isUnskippableSuite) { + this.unskippableSuites.push(relative) + } + }) - const childOf = getTestParentSpan(this.tracer) + const childOf = getTestParentSpan(this.tracer) - const testSessionSpanMetadata = - getTestSessionCommonTags(this.command, this.frameworkVersion, TEST_FRAMEWORK_NAME) - const testModuleSpanMetadata = - getTestModuleCommonTags(this.command, this.frameworkVersion, TEST_FRAMEWORK_NAME) + const testSessionSpanMetadata = + getTestSessionCommonTags(this.command, this.frameworkVersion, TEST_FRAMEWORK_NAME) + const testModuleSpanMetadata = + getTestModuleCommonTags(this.command, this.frameworkVersion, TEST_FRAMEWORK_NAME) - this.testSessionSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_session`, { - childOf, - tags: { - [COMPONENT]: TEST_FRAMEWORK_NAME, - ...this.testEnvironmentMetadata, - ...testSessionSpanMetadata - } - }) - this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'session') - - this.testModuleSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_module`, { - childOf: this.testSessionSpan, - tags: { - [COMPONENT]: TEST_FRAMEWORK_NAME, - ...this.testEnvironmentMetadata, - ...testModuleSpanMetadata - } - }) - this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'module') + if (this.isEarlyFlakeDetectionEnabled) { + testSessionSpanMetadata[TEST_EARLY_FLAKE_IS_ENABLED] = 'true' + } - return details - }) + this.testSessionSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_session`, { + childOf, + tags: { + [COMPONENT]: TEST_FRAMEWORK_NAME, + ...this.testEnvironmentMetadata, + ...testSessionSpanMetadata + } }) + this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'session') + + this.testModuleSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_module`, { + childOf: this.testSessionSpan, + tags: { + [COMPONENT]: TEST_FRAMEWORK_NAME, + ...this.testEnvironmentMetadata, + ...testModuleSpanMetadata + } + }) + this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'module') + + return details } afterRun (suiteStats) { @@ -471,12 +524,18 @@ class CypressPlugin { getTasks () { return { - 'dd:testSuiteStart': (suite) => { + 'dd:testSuiteStart': (testSuite) => { + const suitePayload = { + isEarlyFlakeDetectionEnabled: this.isEarlyFlakeDetectionEnabled, + knownTestsForSuite: this.knownTestsByTestSuite?.[testSuite] || [], + earlyFlakeDetectionNumRetries: this.earlyFlakeDetectionNumRetries + } + if (this.testSuiteSpan) { - return null + return suitePayload } - this.testSuiteSpan = this.getTestSuiteSpan(suite) - return null + this.testSuiteSpan = this.getTestSuiteSpan(testSuite) + return suitePayload }, 'dd:beforeEach': (test) => { const { testName, testSuite } = test @@ -500,7 +559,7 @@ class CypressPlugin { return this.activeTestSpan ? { traceId: this.activeTestSpan.context().toTraceId() } : {} }, 'dd:afterEach': ({ test, coverage }) => { - const { state, error, isRUMActive, testSourceLine, testSuite, testName } = test + const { state, error, isRUMActive, testSourceLine, testSuite, testName, isNew, isEfdRetry } = test if (this.activeTestSpan) { if (coverage && this.isCodeCoverageEnabled && this.tracer._tracer._exporter?.exportCoverage) { const coverageFiles = getCoveredFilenamesFromCoverage(coverage) @@ -530,6 +589,12 @@ class CypressPlugin { if (testSourceLine) { this.activeTestSpan.setTag(TEST_SOURCE_START, testSourceLine) } + if (isNew) { + this.activeTestSpan.setTag(TEST_IS_NEW, 'true') + if (isEfdRetry) { + this.activeTestSpan.setTag(TEST_EARLY_FLAKE_IS_RETRY, 'true') + } + } const finishedTest = { testName, testStatus, diff --git a/packages/datadog-plugin-cypress/src/support.js b/packages/datadog-plugin-cypress/src/support.js index 54def0865d6..6f9b2bbf233 100644 --- a/packages/datadog-plugin-cypress/src/support.js +++ b/packages/datadog-plugin-cypress/src/support.js @@ -1,4 +1,43 @@ /* eslint-disable */ +let isEarlyFlakeDetectionEnabled = false +let knownTestsForSuite = [] +let suiteTests = [] +let earlyFlakeDetectionNumRetries = 0 + +function isNewTest (test) { + return !knownTestsForSuite.includes(test.fullTitle()) +} + +function retryTest (test, suiteTests) { + for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) { + const clonedTest = test.clone() + // TODO: signal in framework logs that this is a retry. + // TODO: Change it so these tests are allowed to fail. + // TODO: figure out if reported duration is skewed. + suiteTests.unshift(clonedTest) + clonedTest._ddIsNew = true + clonedTest._ddIsEfdRetry = true + } +} + + +const oldRunTests = Cypress.mocha.getRunner().runTests +Cypress.mocha.getRunner().runTests = function (suite, fn) { + if (!isEarlyFlakeDetectionEnabled) { + return oldRunTests.apply(this, arguments) + } + // We copy the new tests at the beginning of the suite run (runTests), so that they're run + // multiple times. + suite.tests.forEach(test => { + if (!test._ddIsNew && !test.isPending() && isNewTest(test)) { + test._ddIsNew = true + retryTest(test, suite.tests) + } + }) + + return oldRunTests.apply(this, [suite, fn]) +} + beforeEach(function () { cy.task('dd:beforeEach', { testName: Cypress.mocha.getRunner().suite.ctx.currentTest.fullTitle(), @@ -11,8 +50,14 @@ beforeEach(function () { }) }) -before(() => { - cy.task('dd:testSuiteStart', Cypress.mocha.getRootSuite().file) +before(function () { + cy.task('dd:testSuiteStart', Cypress.mocha.getRootSuite().file).then((suiteConfig) => { + if (suiteConfig) { + isEarlyFlakeDetectionEnabled = suiteConfig.isEarlyFlakeDetectionEnabled + knownTestsForSuite = suiteConfig.knownTestsForSuite + earlyFlakeDetectionNumRetries = suiteConfig.earlyFlakeDetectionNumRetries + } + }) }) after(() => { @@ -24,7 +69,7 @@ after(() => { }) -afterEach(() => { +afterEach(function () { cy.window().then(win => { const currentTest = Cypress.mocha.getRunner().suite.ctx.currentTest const testInfo = { @@ -32,6 +77,8 @@ afterEach(() => { testSuite: Cypress.mocha.getRootSuite().file, state: currentTest.state, error: currentTest.err, + isNew: currentTest._ddIsNew, + isEfdRetry: currentTest._ddIsEfdRetry } try { testInfo.testSourceLine = Cypress.mocha.getRunner().currentRunnable.invocationDetails.line diff --git a/packages/datadog-plugin-graphql/src/index.js b/packages/datadog-plugin-graphql/src/index.js index da0dd01635f..1e530fb94d0 100644 --- a/packages/datadog-plugin-graphql/src/index.js +++ b/packages/datadog-plugin-graphql/src/index.js @@ -1,6 +1,6 @@ 'use strict' -const pick = require('../../utils/src/pick') +const pick = require('../../datadog-core/src/utils/src/pick') const CompositePlugin = require('../../dd-trace/src/plugins/composite') const log = require('../../dd-trace/src/log') const GraphQLExecutePlugin = require('./execute') diff --git a/packages/datadog-plugin-graphql/src/resolve.js b/packages/datadog-plugin-graphql/src/resolve.js index 84c2377fca1..c8d768b0c4e 100644 --- a/packages/datadog-plugin-graphql/src/resolve.js +++ b/packages/datadog-plugin-graphql/src/resolve.js @@ -122,15 +122,17 @@ function getResolverInfo (info, args) { Object.assign(resolverVars, args) } - const directives = info.fieldNodes[0].directives - for (const directive of directives) { - const argList = {} - for (const argument of directive['arguments']) { - argList[argument.name.value] = argument.value.value - } + const directives = info.fieldNodes?.[0]?.directives + if (Array.isArray(directives)) { + for (const directive of directives) { + const argList = {} + for (const argument of directive['arguments']) { + argList[argument.name.value] = argument.value.value + } - if (Object.keys(argList).length) { - resolverVars[directive.name.value] = argList + if (Object.keys(argList).length) { + resolverVars[directive.name.value] = argList + } } } diff --git a/packages/datadog-plugin-graphql/test/index.spec.js b/packages/datadog-plugin-graphql/test/index.spec.js index f45d1a587a4..02831232e9d 100644 --- a/packages/datadog-plugin-graphql/test/index.spec.js +++ b/packages/datadog-plugin-graphql/test/index.spec.js @@ -8,6 +8,7 @@ const { expectedSchema, rawExpectedSchema } = require('./naming') const axios = require('axios') const http = require('http') const getPort = require('get-port') +const dc = require('dc-polyfill') describe('Plugin', () => { let tracer @@ -987,13 +988,31 @@ describe('Plugin', () => { it('should support multiple executions on a pre-parsed document', () => { const source = `query MyQuery { hello(name: "world") }` const document = graphql.parse(source) - expect(() => { graphql.execute({ schema, document }) graphql.execute({ schema, document }) }).to.not.throw() }) + it('should not fail without directives in the document ' + + 'and with subscription to datadog:graphql:resolver:start', () => { + const source = `query MyQuery { hello(name: "world") }` + const document = graphql.parse(source) + delete document.definitions[0].directives + delete document.definitions[0].selectionSet.selections[0].directives + + function noop () {} + dc.channel('datadog:graphql:resolver:start').subscribe(noop) + + try { + expect(() => { + graphql.execute({ schema, document }) + }).to.not.throw() + } finally { + dc.channel('datadog:graphql:resolver:start').unsubscribe(noop) + } + }) + it('should support multiple validations on a pre-parsed document', () => { const source = `query MyQuery { hello(name: "world") }` const document = graphql.parse(source) diff --git a/packages/datadog-plugin-grpc/src/util.js b/packages/datadog-plugin-grpc/src/util.js index f21599a2a1d..1c1937e7ea7 100644 --- a/packages/datadog-plugin-grpc/src/util.js +++ b/packages/datadog-plugin-grpc/src/util.js @@ -1,6 +1,6 @@ 'use strict' -const pick = require('../../utils/src/pick') +const pick = require('../../datadog-core/src/utils/src/pick') const log = require('../../dd-trace/src/log') module.exports = { diff --git a/packages/datadog-plugin-jest/src/index.js b/packages/datadog-plugin-jest/src/index.js index f785bf831c9..8e1422ede51 100644 --- a/packages/datadog-plugin-jest/src/index.js +++ b/packages/datadog-plugin-jest/src/index.js @@ -18,7 +18,8 @@ const { TEST_SOURCE_FILE, TEST_IS_NEW, TEST_EARLY_FLAKE_IS_RETRY, - TEST_EARLY_FLAKE_IS_ENABLED + TEST_EARLY_FLAKE_IS_ENABLED, + JEST_DISPLAY_NAME } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const id = require('../../dd-trace/src/id') @@ -144,7 +145,7 @@ class JestPlugin extends CiPlugin { }) }) - this.addSub('ci:jest:test-suite:start', ({ testSuite, testEnvironmentOptions, frameworkVersion }) => { + this.addSub('ci:jest:test-suite:start', ({ testSuite, testEnvironmentOptions, frameworkVersion, displayName }) => { const { _ddTestSessionId: testSessionId, _ddTestCommand: testCommand, @@ -179,6 +180,9 @@ class JestPlugin extends CiPlugin { if (itrCorrelationId) { testSuiteMetadata[ITR_CORRELATION_ID] = itrCorrelationId } + if (displayName) { + testSuiteMetadata[JEST_DISPLAY_NAME] = displayName + } this.testSuiteSpan = this.tracer.startSpan('jest.test_suite', { childOf: testSessionSpanContext, @@ -308,6 +312,7 @@ class JestPlugin extends CiPlugin { suite, name, runner, + displayName, testParameters, frameworkVersion, testStartLine, @@ -327,6 +332,10 @@ class JestPlugin extends CiPlugin { // If for whatever we don't have the source file, we'll fall back to the suite name extraTags[TEST_SOURCE_FILE] = testSourceFile || suite + if (displayName) { + extraTags[JEST_DISPLAY_NAME] = displayName + } + if (isNew) { extraTags[TEST_IS_NEW] = 'true' if (isEfdRetry) { diff --git a/packages/datadog-plugin-kafkajs/src/consumer.js b/packages/datadog-plugin-kafkajs/src/consumer.js index 2bfbf9fd940..69cb34f642b 100644 --- a/packages/datadog-plugin-kafkajs/src/consumer.js +++ b/packages/datadog-plugin-kafkajs/src/consumer.js @@ -1,7 +1,8 @@ 'use strict' const dc = require('dc-polyfill') -const { getMessageSize, CONTEXT_PROPAGATION_KEY } = require('../../dd-trace/src/datastreams/processor') +const { getMessageSize } = require('../../dd-trace/src/datastreams/processor') +const { DsmPathwayCodec } = require('../../dd-trace/src/datastreams/pathway') const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer') const afterStartCh = dc.channel('dd-trace:kafkajs:consumer:afterStart') @@ -77,9 +78,9 @@ class KafkajsConsumerPlugin extends ConsumerPlugin { 'kafka.partition': partition } }) - if (this.config.dsmEnabled) { + if (this.config.dsmEnabled && message?.headers && DsmPathwayCodec.contextExists(message.headers)) { const payloadSize = getMessageSize(message) - this.tracer.decodeDataStreamsContext(message.headers[CONTEXT_PROPAGATION_KEY]) + this.tracer.decodeDataStreamsContext(message.headers) this.tracer .setCheckpoint(['direction:in', `group:${groupId}`, `topic:${topic}`, 'type:kafka'], span, payloadSize) } diff --git a/packages/datadog-plugin-kafkajs/src/producer.js b/packages/datadog-plugin-kafkajs/src/producer.js index 6c38b2b5689..83c322a1366 100644 --- a/packages/datadog-plugin-kafkajs/src/producer.js +++ b/packages/datadog-plugin-kafkajs/src/producer.js @@ -1,8 +1,8 @@ 'use strict' const ProducerPlugin = require('../../dd-trace/src/plugins/producer') -const { encodePathwayContext } = require('../../dd-trace/src/datastreams/pathway') -const { getMessageSize, CONTEXT_PROPAGATION_KEY } = require('../../dd-trace/src/datastreams/processor') +const { DsmPathwayCodec } = require('../../dd-trace/src/datastreams/pathway') +const { getMessageSize } = require('../../dd-trace/src/datastreams/processor') const BOOTSTRAP_SERVERS_KEY = 'messaging.kafka.bootstrap.servers' @@ -67,7 +67,6 @@ class KafkajsProducerPlugin extends ProducerPlugin { } start ({ topic, messages, bootstrapServers }) { - let pathwayCtx const span = this.startSpan({ resource: topic, meta: { @@ -88,8 +87,7 @@ class KafkajsProducerPlugin extends ProducerPlugin { const payloadSize = getMessageSize(message) const dataStreamsContext = this.tracer .setCheckpoint(['direction:out', `topic:${topic}`, 'type:kafka'], span, payloadSize) - pathwayCtx = encodePathwayContext(dataStreamsContext) - message.headers[CONTEXT_PROPAGATION_KEY] = pathwayCtx + DsmPathwayCodec.encode(dataStreamsContext, message.headers) } } } diff --git a/packages/datadog-plugin-kafkajs/test/index.spec.js b/packages/datadog-plugin-kafkajs/test/index.spec.js index 640aaa98901..c7927c04ba2 100644 --- a/packages/datadog-plugin-kafkajs/test/index.spec.js +++ b/packages/datadog-plugin-kafkajs/test/index.spec.js @@ -273,8 +273,8 @@ describe('Plugin', () => { }) afterStart.subscribe(spy) - consumer.run({ - eachMessage: () => { + let eachMessage = async ({ topic, partition, message }) => { + try { expect(spy).to.have.been.calledOnce const channelMsg = spy.firstCall.args[0] @@ -289,8 +289,15 @@ describe('Plugin', () => { expect(name).to.eq(afterStart.name) done() + } catch (e) { + done(e) + } finally { + eachMessage = () => {} } - }).then(() => sendMessages(kafka, testTopic, messages)) + } + + consumer.run({ eachMessage: (...args) => eachMessage(...args) }) + .then(() => sendMessages(kafka, testTopic, messages)) }) it('should publish on beforeFinish channel', (done) => { @@ -302,15 +309,22 @@ describe('Plugin', () => { }) beforeFinish.subscribe(spy) - consumer.run({ - eachMessage: () => { - setImmediate(() => { + let eachMessage = async ({ topic, partition, message }) => { + setImmediate(() => { + try { expect(spy).to.have.been.calledOnceWith(undefined, beforeFinish.name) done() - }) - } - }).then(() => sendMessages(kafka, testTopic, messages)) + } catch (e) { + done(e) + } + }) + + eachMessage = () => {} + } + + consumer.run({ eachMessage: (...args) => eachMessage(...args) }) + .then(() => sendMessages(kafka, testTopic, messages)) }) withNamingSchema( diff --git a/packages/datadog-plugin-playwright/src/index.js b/packages/datadog-plugin-playwright/src/index.js index 8c8e66c999e..04ac04a9460 100644 --- a/packages/datadog-plugin-playwright/src/index.js +++ b/packages/datadog-plugin-playwright/src/index.js @@ -30,11 +30,23 @@ class PlaywrightPlugin extends CiPlugin { super(...args) this._testSuites = new Map() + this.numFailedTests = 0 + this.numFailedSuites = 0 this.addSub('ci:playwright:session:finish', ({ status, onDone }) => { this.testModuleSpan.setTag(TEST_STATUS, status) this.testSessionSpan.setTag(TEST_STATUS, status) + 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) + } + this.testModuleSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module') this.testSessionSpan.finish() @@ -42,6 +54,7 @@ class PlaywrightPlugin extends CiPlugin { finishAllTraceSpans(this.testSessionSpan) appClosingTelemetry() this.tracer._exporter.flush(onDone) + this.numFailedTests = 0 }) this.addSub('ci:playwright:test-suite:start', (testSuiteAbsolutePath) => { @@ -69,11 +82,21 @@ class PlaywrightPlugin extends CiPlugin { this._testSuites.set(testSuite, testSuiteSpan) }) - this.addSub('ci:playwright:test-suite:finish', (status) => { + this.addSub('ci:playwright:test-suite:finish', ({ status, error }) => { const store = storage.getStore() const span = store && store.span if (!span) return - span.setTag(TEST_STATUS, status) + if (error) { + span.setTag('error', error) + span.setTag(TEST_STATUS, 'fail') + } else { + span.setTag(TEST_STATUS, status) + } + + if (status === 'fail' || error) { + this.numFailedSuites++ + } + span.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite') }) @@ -114,11 +137,19 @@ class PlaywrightPlugin extends CiPlugin { if (step.error) { stepSpan.setTag('error', step.error) } - stepSpan.finish(stepStartTime + step.duration) + let stepDuration = step.duration + if (stepDuration <= 0 || isNaN(stepDuration)) { + stepDuration = 0 + } + stepSpan.finish(stepStartTime + stepDuration) }) span.finish() + if (testStatus === 'fail') { + this.numFailedTests++ + } + this.telemetry.ciVisEvent( TELEMETRY_EVENT_FINISHED, 'test', diff --git a/packages/datadog-plugin-rhea/src/consumer.js b/packages/datadog-plugin-rhea/src/consumer.js index b67a617234d..73c83be1f44 100644 --- a/packages/datadog-plugin-rhea/src/consumer.js +++ b/packages/datadog-plugin-rhea/src/consumer.js @@ -2,7 +2,8 @@ const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer') const { storage } = require('../../datadog-core') -const { getAmqpMessageSize, CONTEXT_PROPAGATION_KEY } = require('../../dd-trace/src/datastreams/processor') +const { getAmqpMessageSize } = require('../../dd-trace/src/datastreams/processor') +const { DsmPathwayCodec } = require('../../dd-trace/src/datastreams/pathway') class RheaConsumerPlugin extends ConsumerPlugin { static get id () { return 'rhea' } @@ -33,12 +34,13 @@ class RheaConsumerPlugin extends ConsumerPlugin { if ( this.config.dsmEnabled && - msgObj?.message?.delivery_annotations?.[CONTEXT_PROPAGATION_KEY] + msgObj?.message?.delivery_annotations && + DsmPathwayCodec.contextExists(msgObj.message.delivery_annotations) ) { const payloadSize = getAmqpMessageSize( { headers: msgObj.message.delivery_annotations, content: msgObj.message.body } ) - this.tracer.decodeDataStreamsContext(msgObj.message.delivery_annotations[CONTEXT_PROPAGATION_KEY]) + this.tracer.decodeDataStreamsContext(msgObj.message.delivery_annotations) this.tracer .setCheckpoint(['direction:in', `topic:${name}`, 'type:rabbitmq'], span, payloadSize) } diff --git a/packages/datadog-plugin-rhea/src/producer.js b/packages/datadog-plugin-rhea/src/producer.js index f79dd368e60..991f64371ab 100644 --- a/packages/datadog-plugin-rhea/src/producer.js +++ b/packages/datadog-plugin-rhea/src/producer.js @@ -2,8 +2,8 @@ const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants') const ProducerPlugin = require('../../dd-trace/src/plugins/producer') -const { encodePathwayContext } = require('../../dd-trace/src/datastreams/pathway') -const { getAmqpMessageSize, CONTEXT_PROPAGATION_KEY } = require('../../dd-trace/src/datastreams/processor') +const { DsmPathwayCodec } = require('../../dd-trace/src/datastreams/pathway') +const { getAmqpMessageSize } = require('../../dd-trace/src/datastreams/processor') class RheaProducerPlugin extends ProducerPlugin { static get id () { return 'rhea' } @@ -44,8 +44,7 @@ function addDeliveryAnnotations (msg, tracer, span) { const payloadSize = getAmqpMessageSize({ content: msg.body, headers: msg.delivery_annotations }) const dataStreamsContext = tracer .setCheckpoint(['direction:out', `exchange:${targetName}`, 'type:rabbitmq'], span, payloadSize) - const pathwayCtx = encodePathwayContext(dataStreamsContext) - msg.delivery_annotations[CONTEXT_PROPAGATION_KEY] = pathwayCtx + DsmPathwayCodec.encode(dataStreamsContext, msg.delivery_annotations) } } } diff --git a/packages/dd-trace/src/appsec/iast/index.js b/packages/dd-trace/src/appsec/iast/index.js index 11745e59701..0facaa39a2a 100644 --- a/packages/dd-trace/src/appsec/iast/index.js +++ b/packages/dd-trace/src/appsec/iast/index.js @@ -21,7 +21,11 @@ const requestStart = dc.channel('dd-trace:incomingHttpRequestStart') const requestClose = dc.channel('dd-trace:incomingHttpRequestEnd') const iastResponseEnd = dc.channel('datadog:iast:response-end') +let isEnabled = false + function enable (config, _tracer) { + if (isEnabled) return + iastTelemetry.configure(config, config.iast?.telemetryVerbosity) enableAllAnalyzers(config) enableTaintTracking(config.iast, iastTelemetry.verbosity) @@ -30,9 +34,15 @@ function enable (config, _tracer) { overheadController.configure(config.iast) overheadController.startGlobalContext() vulnerabilityReporter.start(config, _tracer) + + isEnabled = true } function disable () { + if (!isEnabled) return + + isEnabled = false + iastTelemetry.stop() disableAllAnalyzers() disableTaintTracking() diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js b/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js index 71f6265b81d..a2ab09c6f9f 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js @@ -65,16 +65,18 @@ function getRewriter (telemetryVerbosity) { return rewriter } -let originalPrepareStackTrace = Error.prepareStackTrace +let originalPrepareStackTrace +let actualPrepareStackTrace function getPrepareStackTraceAccessor () { - let actual = getPrepareStackTrace(originalPrepareStackTrace) + originalPrepareStackTrace = Error.prepareStackTrace + actualPrepareStackTrace = getPrepareStackTrace(originalPrepareStackTrace) return { configurable: true, get () { - return actual + return actualPrepareStackTrace }, set (value) { - actual = getPrepareStackTrace(value) + actualPrepareStackTrace = getPrepareStackTrace(value) originalPrepareStackTrace = value } } @@ -121,7 +123,18 @@ function enableRewriter (telemetryVerbosity) { function disableRewriter () { shimmer.unwrap(Module.prototype, '_compile') - Error.prepareStackTrace = originalPrepareStackTrace + + if (!actualPrepareStackTrace) return + + try { + delete Error.prepareStackTrace + + Error.prepareStackTrace = originalPrepareStackTrace + + actualPrepareStackTrace = undefined + } catch (e) { + iastLog.warn(e) + } } function getOriginalPathAndLineFromSourceMap ({ path, line, column }) { diff --git a/packages/dd-trace/src/appsec/recommended.json b/packages/dd-trace/src/appsec/recommended.json index d572c003911..7912743b40a 100644 --- a/packages/dd-trace/src/appsec/recommended.json +++ b/packages/dd-trace/src/appsec/recommended.json @@ -1,7 +1,7 @@ { "version": "2.2", "metadata": { - "rules_version": "1.10.0" + "rules_version": "1.11.0" }, "rules": [ { @@ -141,7 +141,10 @@ "appscan_fingerprint", "w00tw00t.at.isc.sans.dfind", "w00tw00t.at.blackhats.romanian.anti-sec" - ] + ], + "options": { + "enforce_word_boundary": true + } }, "operator": "phrase_match" } @@ -1778,7 +1781,10 @@ "windows\\win.ini", "default\\ntuser.dat", "/var/run/secrets/kubernetes.io/serviceaccount" - ] + ], + "options": { + "enforce_word_boundary": true + } }, "operator": "phrase_match" } @@ -1895,6 +1901,9 @@ "address": "graphql.server.resolver" } ], + "options": { + "enforce_word_boundary": true + }, "list": [ "${cdpath}", "${dirstack}", @@ -2471,7 +2480,10 @@ "settings.local.php", "local.xml", ".env" - ] + ], + "options": { + "enforce_word_boundary": true + } }, "operator": "phrase_match" } @@ -2567,6 +2579,9 @@ "address": "graphql.server.resolver" } ], + "options": { + "enforce_word_boundary": true + }, "list": [ "$globals", "$_cookie", @@ -2765,7 +2780,10 @@ "wp_safe_remote_post", "wp_safe_remote_request", "zlib_decode" - ] + ], + "options": { + "enforce_word_boundary": true + } }, "operator": "phrase_match" } @@ -2980,9 +2998,6 @@ { "address": "server.request.path_params" }, - { - "address": "grpc.server.request.message" - }, { "address": "graphql.server.all_resolvers" }, @@ -3037,9 +3052,6 @@ { "address": "server.request.path_params" }, - { - "address": "grpc.server.request.message" - }, { "address": "graphql.server.all_resolvers" }, @@ -3271,6 +3283,9 @@ "address": "graphql.server.resolver" } ], + "options": { + "enforce_word_boundary": true + }, "list": [ "document.cookie", "document.write", @@ -3546,9 +3561,6 @@ { "address": "server.request.path_params" }, - { - "address": "grpc.server.request.message" - }, { "address": "graphql.server.all_resolvers" }, @@ -3863,9 +3875,6 @@ { "address": "server.request.path_params" }, - { - "address": "grpc.server.request.message" - }, { "address": "graphql.server.all_resolvers" }, @@ -4454,7 +4463,10 @@ "org.apache.struts2", "org.omg.corba", "java.beans.xmldecode" - ] + ], + "options": { + "enforce_word_boundary": true + } }, "operator": "phrase_match" } @@ -4581,9 +4593,6 @@ { "address": "server.request.path_params" }, - { - "address": "grpc.server.request.message" - }, { "address": "graphql.server.all_resolvers" }, @@ -5342,6 +5351,40 @@ ], "transformers": [] }, + { + "id": "dog-920-001", + "name": "JWT authentication bypass", + "tags": { + "type": "http_protocol_violation", + "category": "attack_attempt", + "cwe": "287", + "capec": "1000/225/115", + "confidence": "0" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "authorization" + ] + } + ], + "regex": "^(?:Bearer )?ey[A-Za-z0-9+_\\-/]*([QY][UW]x[Hn]Ij([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiAi[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]Ij([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDogI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]IiA6ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]Ij([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiAi[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciOiAi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciIDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgOiJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]IiA6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]Ij([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciOiJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgOiAi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]IjogI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]IiA6I[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6I[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6I[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciIDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]IiA6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]IiA6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6I[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]IiA6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6I[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]Ijoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiAi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f])[A-Za-z0-9+-/]*\\.[A-Za-z0-9+_\\-/]+\\.(?:[A-Za-z0-9+_\\-/]+)?$", + "options": { + "case_sensitive": true + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, { "id": "dog-931-001", "name": "RFI: URL Payload to well known RFI target", @@ -5603,6 +5646,9 @@ { "operator": "phrase_match", "parameters": { + "options": { + "enforce_word_boundary": true + }, "inputs": [ { "address": "server.request.uri.raw" @@ -6606,9 +6652,6 @@ { "address": "server.request.headers.no_cookies" }, - { - "address": "grpc.server.request.message" - }, { "address": "graphql.server.all_resolvers" }, @@ -6654,9 +6697,6 @@ { "address": "server.request.headers.no_cookies" }, - { - "address": "grpc.server.request.message" - }, { "address": "graphql.server.all_resolvers" }, diff --git a/packages/dd-trace/src/appsec/remote_config/index.js b/packages/dd-trace/src/appsec/remote_config/index.js index 7201d6a5d5b..dea378e9244 100644 --- a/packages/dd-trace/src/appsec/remote_config/index.js +++ b/packages/dd-trace/src/appsec/remote_config/index.js @@ -60,7 +60,7 @@ function enableOrDisableAppsec (action, rcConfig, config) { } function enableWafUpdate (appsecConfig) { - if (rc && appsecConfig && !appsecConfig.customRulesProvided) { + if (rc && appsecConfig && !appsecConfig.rules) { // dirty require to make startup faster for serverless const RuleManager = require('../rule_manager') diff --git a/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js b/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js index 144a1c006bd..16f7337c7ae 100644 --- a/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +++ b/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js @@ -1,6 +1,5 @@ const request = require('../../exporters/common/request') const id = require('../../id') -const log = require('../../log') function getKnownTests ({ url, @@ -70,8 +69,7 @@ function getKnownTests ({ done(err) } else { try { - const { data: { attributes: { test_full_names: knownTests } } } = JSON.parse(res) - log.debug(() => `Number of received known tests: ${Object.keys(knownTests).length}`) + const { data: { attributes: { tests: knownTests } } } = JSON.parse(res) done(null, knownTests) } catch (err) { done(err) diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index ecaf522e0bb..c8aef6e6248 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -8,6 +8,9 @@ const log = require('./log') const pkg = require('./pkg') const coalesce = require('koalas') const tagger = require('./tagger') +const get = require('../../datadog-core/src/utils/src/get') +const has = require('../../datadog-core/src/utils/src/has') +const set = require('../../datadog-core/src/utils/src/set') const { isTrue, isFalse } = require('./util') const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('./plugins/util/tags') const { getGitMetadataFromGitProperties, removeUserSensitiveInfo } = require('./git_properties') @@ -20,6 +23,13 @@ const fromEntries = Object.fromEntries || (entries => // eslint-disable-next-line max-len const qsRegex = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\\s|%20)*(?:=|%3D)[^&]+|(?:"|%22)(?:\\s|%20)*(?::|%3A)(?:\\s|%20)*(?:"|%22)(?:%2[^2]|%[^2]|[^"%])+(?:"|%22))|bearer(?:\\s|%20)+[a-z0-9\\._\\-]+|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\\w=-]|%3D)+\\.ey[I-L](?:[\\w=-]|%3D)+(?:\\.(?:[\\w.+\\/=-]|%3D|%2F|%2B)+)?|[\\-]{5}BEGIN(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY[\\-]{5}[^\\-]+[\\-]{5}END(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY|ssh-rsa(?:\\s|%20)*(?:[a-z0-9\\/\\.+]|%2F|%5C|%2B){100,}' +const defaultWafObfuscatorKeyRegex = `(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?\ +|public_?)key)|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization` +const defaultWafObfuscatorValueRegex = +`(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|to\ +ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:\\s*=[^;]|"\\s*:\\s*"[^"]+")|bearer\ +\\s+[a-z0-9\\._\\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=-]+\\.ey[I-L][\\w=-]+(?:\\.[\\w.+\\/=-]+)?\ +|[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*[a-z0-9\\/\\.+]{100,}` function maybeFile (filepath) { if (!filepath) return @@ -94,6 +104,11 @@ function propagationStyle (key, option, defaultValue) { class Config { constructor (options) { options = options || {} + options = this.options = { + ...options, + appsec: options.appsec != null ? options.appsec : options.experimental?.appsec, + iastOptions: options.experimental?.iast + } // Configure the logger first so it can be used to warn about other configs this.debug = isTrue(coalesce( @@ -110,70 +125,6 @@ class Config { log.use(this.logger) log.toggle(this.debug, this.logLevel, this) - const DD_PROFILING_ENABLED = coalesce( - options.profiling, // TODO: remove when enabled by default - process.env.DD_EXPERIMENTAL_PROFILING_ENABLED, - process.env.DD_PROFILING_ENABLED, - false - ) - const DD_PROFILING_EXPORTERS = coalesce( - process.env.DD_PROFILING_EXPORTERS, - 'agent' - ) - const DD_PROFILING_SOURCE_MAP = process.env.DD_PROFILING_SOURCE_MAP - const DD_RUNTIME_METRICS_ENABLED = coalesce( - options.runtimeMetrics, // TODO: remove when enabled by default - process.env.DD_RUNTIME_METRICS_ENABLED, - false - ) - const DD_DBM_PROPAGATION_MODE = coalesce( - options.dbmPropagationMode, - process.env.DD_DBM_PROPAGATION_MODE, - 'disabled' - ) - const DD_DATA_STREAMS_ENABLED = coalesce( - options.dsmEnabled, - process.env.DD_DATA_STREAMS_ENABLED, - false - ) - const DD_AGENT_HOST = coalesce( - options.hostname, - process.env.DD_AGENT_HOST, - process.env.DD_TRACE_AGENT_HOSTNAME, - '127.0.0.1' - ) - const DD_TRACE_AGENT_PORT = coalesce( - options.port, - process.env.DD_TRACE_AGENT_PORT, - '8126' - ) - const DD_TRACE_AGENT_URL = coalesce( - options.url, - process.env.DD_TRACE_AGENT_URL, - process.env.DD_TRACE_URL, - null - ) - const DD_IS_CIVISIBILITY = coalesce( - options.isCiVisibility, - false - ) - const DD_CIVISIBILITY_AGENTLESS_URL = process.env.DD_CIVISIBILITY_AGENTLESS_URL - - const DD_CIVISIBILITY_ITR_ENABLED = coalesce( - process.env.DD_CIVISIBILITY_ITR_ENABLED, - true - ) - - const DD_CIVISIBILITY_MANUAL_API_ENABLED = coalesce( - process.env.DD_CIVISIBILITY_MANUAL_API_ENABLED, - false - ) - - const DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED = coalesce( - process.env.DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED, - true - ) - const DD_TRACE_MEMCACHED_COMMAND_ENABLED = coalesce( process.env.DD_TRACE_MEMCACHED_COMMAND_ENABLED, false @@ -185,75 +136,12 @@ class Config { process.env.DD_SERVICE_MAPPING.split(',').map(x => x.trim().split(':')) ) : {} ) - const DD_TRACE_STARTUP_LOGS = coalesce( - options.startupLogs, - process.env.DD_TRACE_STARTUP_LOGS, - false - ) - - const DD_OPENAI_LOGS_ENABLED = coalesce( - options.openAiLogsEnabled, - process.env.DD_OPENAI_LOGS_ENABLED, - false - ) const DD_API_KEY = coalesce( process.env.DATADOG_API_KEY, process.env.DD_API_KEY ) - const inAWSLambda = process.env.AWS_LAMBDA_FUNCTION_NAME !== undefined - - const isGCPFunction = getIsGCPFunction() - const isAzureFunctionConsumptionPlan = getIsAzureFunctionConsumptionPlan() - - const inServerlessEnvironment = inAWSLambda || isGCPFunction || isAzureFunctionConsumptionPlan - - const isJestWorker = !!process.env.JEST_WORKER_ID - - const DD_INSTRUMENTATION_TELEMETRY_ENABLED = coalesce( - process.env.DD_TRACE_TELEMETRY_ENABLED, // for backward compatibility - process.env.DD_INSTRUMENTATION_TELEMETRY_ENABLED, // to comply with instrumentation telemetry specs - !(inServerlessEnvironment || isJestWorker) - ) - const DD_TELEMETRY_HEARTBEAT_INTERVAL = process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL - ? Math.floor(parseFloat(process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL) * 1000) - : 60000 - const DD_OPENAI_SPAN_CHAR_LIMIT = process.env.DD_OPENAI_SPAN_CHAR_LIMIT - ? parseInt(process.env.DD_OPENAI_SPAN_CHAR_LIMIT) - : 128 - const DD_TELEMETRY_DEBUG = coalesce( - process.env.DD_TELEMETRY_DEBUG, - false - ) - const DD_TELEMETRY_METRICS_ENABLED = coalesce( - process.env.DD_TELEMETRY_METRICS_ENABLED, - true - ) - const DD_TRACE_AGENT_PROTOCOL_VERSION = coalesce( - options.protocolVersion, - process.env.DD_TRACE_AGENT_PROTOCOL_VERSION, - '0.4' - ) - const DD_TRACE_PARTIAL_FLUSH_MIN_SPANS = coalesce( - parseInt(options.flushMinSpans), - parseInt(process.env.DD_TRACE_PARTIAL_FLUSH_MIN_SPANS), - 1000 - ) - const DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP = coalesce( - process.env.DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP, - qsRegex - ) - const DD_TRACE_CLIENT_IP_ENABLED = coalesce( - options.clientIpEnabled, - process.env.DD_TRACE_CLIENT_IP_ENABLED && isTrue(process.env.DD_TRACE_CLIENT_IP_ENABLED), - false - ) - const DD_TRACE_CLIENT_IP_HEADER = coalesce( - options.clientIpHeader, - process.env.DD_TRACE_CLIENT_IP_HEADER, - null - ) // TODO: Remove the experimental env vars as a major? const DD_TRACE_B3_ENABLED = coalesce( options.experimental && options.experimental.b3, @@ -289,238 +177,36 @@ class Config { process.env.DD_TRACE_PROPAGATION_EXTRACT_FIRST, false ) - const DD_TRACE_RUNTIME_ID_ENABLED = coalesce( - options.experimental && options.experimental.runtimeId, - process.env.DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED, - false - ) - const DD_TRACE_EXPORTER = coalesce( - options.experimental && options.experimental.exporter, - process.env.DD_TRACE_EXPERIMENTAL_EXPORTER - ) - const DD_TRACE_GET_RUM_DATA_ENABLED = coalesce( - options.experimental && options.experimental.enableGetRumData, - process.env.DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED, - false - ) - const DD_TRACE_SPAN_ATTRIBUTE_SCHEMA = validateNamingVersion( - coalesce( - options.spanAttributeSchema, - process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA - ) - ) - const DD_TRACE_PEER_SERVICE_MAPPING = coalesce( - options.peerServiceMapping, - process.env.DD_TRACE_PEER_SERVICE_MAPPING ? fromEntries( - process.env.DD_TRACE_PEER_SERVICE_MAPPING.split(',').map(x => x.trim().split(':')) - ) : {} - ) - - const peerServiceSet = ( - options.hasOwnProperty('spanComputePeerService') || - process.env.hasOwnProperty('DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED') - ) - const peerServiceValue = coalesce( - options.spanComputePeerService, - process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED - ) - - const DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = ( - DD_TRACE_SPAN_ATTRIBUTE_SCHEMA === 'v0' - // In v0, peer service is computed only if it is explicitly set to true - ? peerServiceSet && isTrue(peerServiceValue) - // In >v0, peer service is false only if it is explicitly set to false - : (peerServiceSet ? !isFalse(peerServiceValue) : true) - ) - - const DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED = coalesce( - options.spanRemoveIntegrationFromService, - isTrue(process.env.DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED) - ) - const DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH = coalesce( - process.env.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, - '512' - ) - - const DD_TRACE_STATS_COMPUTATION_ENABLED = coalesce( - options.stats, - process.env.DD_TRACE_STATS_COMPUTATION_ENABLED, - isGCPFunction || isAzureFunctionConsumptionPlan - ) - - // the tracer generates 128 bit IDs by default as of v5 - const DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED = coalesce( - options.traceId128BitGenerationEnabled, - process.env.DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED, - true - ) - - const DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED = coalesce( - options.traceId128BitLoggingEnabled, - process.env.DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED, - false - ) - let appsec = options.appsec != null ? options.appsec : options.experimental && options.experimental.appsec - - if (typeof appsec === 'boolean') { - appsec = { - enabled: appsec + if (typeof options.appsec === 'boolean') { + options.appsec = { + enabled: options.appsec } - } else if (appsec == null) { - appsec = {} + } else if (options.appsec == null) { + options.appsec = {} } - const DD_APPSEC_ENABLED = coalesce( - appsec.enabled, - process.env.DD_APPSEC_ENABLED && isTrue(process.env.DD_APPSEC_ENABLED) - ) - const DD_APPSEC_RULES = coalesce( - appsec.rules, - process.env.DD_APPSEC_RULES - ) - const DD_APPSEC_TRACE_RATE_LIMIT = coalesce( - parseInt(appsec.rateLimit), - parseInt(process.env.DD_APPSEC_TRACE_RATE_LIMIT), - 100 - ) - const DD_APPSEC_WAF_TIMEOUT = coalesce( - parseInt(appsec.wafTimeout), - parseInt(process.env.DD_APPSEC_WAF_TIMEOUT), - 5e3 // µs - ) - const DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP = coalesce( - appsec.obfuscatorKeyRegex, - process.env.DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP, - `(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?)key)|token|consumer_?(?:id|key|se\ -cret)|sign(?:ed|ature)|bearer|authorization` - ) - const DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP = coalesce( - appsec.obfuscatorValueRegex, - process.env.DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP, - `(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|to\ -ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:\\s*=[^;]|"\\s*:\\s*"[^"]+")|bearer\ -\\s+[a-z0-9\\._\\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=-]+\\.ey[I-L][\\w=-]+(?:\\.[\\w.+\\/=-]+)?\ -|[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*[a-z0-9\\/\\.+]{100,}` - ) - const DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML = coalesce( - maybeFile(appsec.blockedTemplateHtml), - maybeFile(process.env.DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML) - ) - const DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON = coalesce( - maybeFile(appsec.blockedTemplateJson), - maybeFile(process.env.DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON) - ) const DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON = coalesce( - maybeFile(appsec.blockedTemplateGraphql), + maybeFile(options.appsec.blockedTemplateGraphql), maybeFile(process.env.DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON) ) const DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING = coalesce( - appsec.eventTracking && appsec.eventTracking.mode, + options.appsec.eventTracking && options.appsec.eventTracking.mode, process.env.DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING, 'safe' ).toLowerCase() const DD_API_SECURITY_ENABLED = coalesce( - appsec?.apiSecurity?.enabled, + options.appsec?.apiSecurity?.enabled, process.env.DD_API_SECURITY_ENABLED && isTrue(process.env.DD_API_SECURITY_ENABLED), process.env.DD_EXPERIMENTAL_API_SECURITY_ENABLED && isTrue(process.env.DD_EXPERIMENTAL_API_SECURITY_ENABLED), true ) const DD_API_SECURITY_REQUEST_SAMPLE_RATE = coalesce( - appsec?.apiSecurity?.requestSampling, + options.appsec?.apiSecurity?.requestSampling, parseFloat(process.env.DD_API_SECURITY_REQUEST_SAMPLE_RATE), 0.1 ) - const remoteConfigOptions = options.remoteConfig || {} - const DD_REMOTE_CONFIGURATION_ENABLED = coalesce( - process.env.DD_REMOTE_CONFIGURATION_ENABLED && isTrue(process.env.DD_REMOTE_CONFIGURATION_ENABLED), - !inServerlessEnvironment - ) - const DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS = coalesce( - parseFloat(remoteConfigOptions.pollInterval), - parseFloat(process.env.DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS), - 5 // seconds - ) - - const iastOptions = options?.experimental?.iast - const DD_IAST_ENABLED = coalesce( - iastOptions && - (iastOptions === true || iastOptions.enabled === true), - process.env.DD_IAST_ENABLED, - false - ) - const DD_TELEMETRY_LOG_COLLECTION_ENABLED = coalesce( - process.env.DD_TELEMETRY_LOG_COLLECTION_ENABLED, - DD_IAST_ENABLED - ) - - const DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED = coalesce( - process.env.DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED, - true - ) - - const defaultIastRequestSampling = 30 - const iastRequestSampling = coalesce( - parseInt(iastOptions?.requestSampling), - parseInt(process.env.DD_IAST_REQUEST_SAMPLING), - defaultIastRequestSampling - ) - const DD_IAST_REQUEST_SAMPLING = iastRequestSampling < 0 || - iastRequestSampling > 100 ? defaultIastRequestSampling : iastRequestSampling - - const DD_IAST_MAX_CONCURRENT_REQUESTS = coalesce( - parseInt(iastOptions?.maxConcurrentRequests), - parseInt(process.env.DD_IAST_MAX_CONCURRENT_REQUESTS), - 2 - ) - - const DD_IAST_MAX_CONTEXT_OPERATIONS = coalesce( - parseInt(iastOptions?.maxContextOperations), - parseInt(process.env.DD_IAST_MAX_CONTEXT_OPERATIONS), - 2 - ) - - const DD_IAST_DEDUPLICATION_ENABLED = coalesce( - iastOptions?.deduplicationEnabled, - process.env.DD_IAST_DEDUPLICATION_ENABLED && isTrue(process.env.DD_IAST_DEDUPLICATION_ENABLED), - true - ) - - const DD_IAST_REDACTION_ENABLED = coalesce( - iastOptions?.redactionEnabled, - !isFalse(process.env.DD_IAST_REDACTION_ENABLED), - true - ) - - const DD_IAST_REDACTION_NAME_PATTERN = coalesce( - iastOptions?.redactionNamePattern, - process.env.DD_IAST_REDACTION_NAME_PATTERN, - null - ) - - const DD_IAST_REDACTION_VALUE_PATTERN = coalesce( - iastOptions?.redactionValuePattern, - process.env.DD_IAST_REDACTION_VALUE_PATTERN, - null - ) - - const DD_IAST_TELEMETRY_VERBOSITY = coalesce( - iastOptions?.telemetryVerbosity, - process.env.DD_IAST_TELEMETRY_VERBOSITY, - 'INFORMATION' - ) - - const DD_CIVISIBILITY_GIT_UPLOAD_ENABLED = coalesce( - process.env.DD_CIVISIBILITY_GIT_UPLOAD_ENABLED, - true - ) - - const DD_TRACE_GIT_METADATA_ENABLED = coalesce( - process.env.DD_TRACE_GIT_METADATA_ENABLED, - true - ) - // 0: disabled, 1: logging, 2: garbage collection + logging const DD_TRACE_SPAN_LEAK_DEBUG = coalesce( process.env.DD_TRACE_SPAN_LEAK_DEBUG, @@ -540,10 +226,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) null ) - const ingestion = options.ingestion || {} - const dogstatsd = coalesce(options.dogstatsd, {}) const sampler = { - rateLimit: coalesce(options.rateLimit, process.env.DD_TRACE_RATE_LIMIT, ingestion.rateLimit), rules: coalesce( options.samplingRules, safeJsonParse(process.env.DD_TRACE_SAMPLING_RULES), @@ -566,73 +249,16 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) }) } - const defaultFlushInterval = inAWSLambda ? 0 : 2000 - - this.dbmPropagationMode = DD_DBM_PROPAGATION_MODE - this.dsmEnabled = isTrue(DD_DATA_STREAMS_ENABLED) - this.openAiLogsEnabled = DD_OPENAI_LOGS_ENABLED + // TODO: refactor this.apiKey = DD_API_KEY - this.url = DD_CIVISIBILITY_AGENTLESS_URL ? new URL(DD_CIVISIBILITY_AGENTLESS_URL) - : getAgentUrl(DD_TRACE_AGENT_URL, options) - this.site = coalesce(options.site, process.env.DD_SITE, 'datadoghq.com') - this.hostname = DD_AGENT_HOST || (this.url && this.url.hostname) - this.port = String(DD_TRACE_AGENT_PORT || (this.url && this.url.port)) - this.flushInterval = coalesce(parseInt(options.flushInterval, 10), defaultFlushInterval) - this.flushMinSpans = DD_TRACE_PARTIAL_FLUSH_MIN_SPANS - this.queryStringObfuscation = DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP - this.clientIpEnabled = DD_TRACE_CLIENT_IP_ENABLED - this.clientIpHeader = DD_TRACE_CLIENT_IP_HEADER - this.plugins = !!coalesce(options.plugins, true) this.serviceMapping = DD_SERVICE_MAPPING - this.dogstatsd = { - hostname: coalesce(dogstatsd.hostname, process.env.DD_DOGSTATSD_HOSTNAME, this.hostname), - port: String(coalesce(dogstatsd.port, process.env.DD_DOGSTATSD_PORT, 8125)) - } - this.runtimeMetrics = isTrue(DD_RUNTIME_METRICS_ENABLED) this.tracePropagationStyle = { inject: DD_TRACE_PROPAGATION_STYLE_INJECT, extract: DD_TRACE_PROPAGATION_STYLE_EXTRACT } this.tracePropagationExtractFirst = isTrue(DD_TRACE_PROPAGATION_EXTRACT_FIRST) - this.experimental = { - runtimeId: isTrue(DD_TRACE_RUNTIME_ID_ENABLED), - exporter: DD_TRACE_EXPORTER, - enableGetRumData: isTrue(DD_TRACE_GET_RUM_DATA_ENABLED) - } this.sampler = sampler - this.reportHostname = isTrue(coalesce(options.reportHostname, process.env.DD_TRACE_REPORT_HOSTNAME, false)) - this.scope = process.env.DD_TRACE_SCOPE - this.profiling = { - enabled: isTrue(DD_PROFILING_ENABLED), - sourceMap: !isFalse(DD_PROFILING_SOURCE_MAP), - exporters: DD_PROFILING_EXPORTERS - } - this.spanAttributeSchema = DD_TRACE_SPAN_ATTRIBUTE_SCHEMA - this.spanComputePeerService = DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED - this.spanRemoveIntegrationFromService = DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED - this.peerServiceMapping = DD_TRACE_PEER_SERVICE_MAPPING - this.lookup = options.lookup - this.startupLogs = isTrue(DD_TRACE_STARTUP_LOGS) - this.telemetry = { - enabled: isTrue(DD_INSTRUMENTATION_TELEMETRY_ENABLED), - heartbeatInterval: DD_TELEMETRY_HEARTBEAT_INTERVAL, - debug: isTrue(DD_TELEMETRY_DEBUG), - logCollection: isTrue(DD_TELEMETRY_LOG_COLLECTION_ENABLED), - metrics: isTrue(DD_TELEMETRY_METRICS_ENABLED), - dependencyCollection: DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED - } - this.protocolVersion = DD_TRACE_AGENT_PROTOCOL_VERSION - this.tagsHeaderMaxLength = parseInt(DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH) this.appsec = { - enabled: DD_APPSEC_ENABLED, - rules: DD_APPSEC_RULES, - customRulesProvided: !!DD_APPSEC_RULES, - rateLimit: DD_APPSEC_TRACE_RATE_LIMIT, - wafTimeout: DD_APPSEC_WAF_TIMEOUT, - obfuscatorKeyRegex: DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP, - obfuscatorValueRegex: DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP, - blockedTemplateHtml: DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML, - blockedTemplateJson: DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON, blockedTemplateGraphql: DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON, eventTracking: { enabled: ['extended', 'safe'].includes(DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING), @@ -645,49 +271,10 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) } } - this.remoteConfig = { - enabled: DD_REMOTE_CONFIGURATION_ENABLED, - pollInterval: DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS - } - this.iast = { - enabled: isTrue(DD_IAST_ENABLED), - requestSampling: DD_IAST_REQUEST_SAMPLING, - maxConcurrentRequests: DD_IAST_MAX_CONCURRENT_REQUESTS, - maxContextOperations: DD_IAST_MAX_CONTEXT_OPERATIONS, - deduplicationEnabled: DD_IAST_DEDUPLICATION_ENABLED, - redactionEnabled: DD_IAST_REDACTION_ENABLED, - redactionNamePattern: DD_IAST_REDACTION_NAME_PATTERN, - redactionValuePattern: DD_IAST_REDACTION_VALUE_PATTERN, - telemetryVerbosity: DD_IAST_TELEMETRY_VERBOSITY - } - - this.isCiVisibility = isTrue(DD_IS_CIVISIBILITY) - - this.isIntelligentTestRunnerEnabled = this.isCiVisibility && isTrue(DD_CIVISIBILITY_ITR_ENABLED) - this.isGitUploadEnabled = this.isCiVisibility && - (this.isIntelligentTestRunnerEnabled && !isFalse(DD_CIVISIBILITY_GIT_UPLOAD_ENABLED)) - - this.gitMetadataEnabled = isTrue(DD_TRACE_GIT_METADATA_ENABLED) - this.isManualApiEnabled = this.isCiVisibility && isTrue(DD_CIVISIBILITY_MANUAL_API_ENABLED) - this.isEarlyFlakeDetectionEnabled = this.isCiVisibility && isTrue(DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED) - - this.openaiSpanCharLimit = DD_OPENAI_SPAN_CHAR_LIMIT - // Requires an accompanying DD_APM_OBFUSCATION_MEMCACHED_KEEP_COMMAND=true in the agent this.memcachedCommandEnabled = isTrue(DD_TRACE_MEMCACHED_COMMAND_ENABLED) - - this.stats = { - enabled: isTrue(DD_TRACE_STATS_COMPUTATION_ENABLED) - } - - this.traceId128BitGenerationEnabled = isTrue(DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED) - this.traceId128BitLoggingEnabled = isTrue(DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED) - - this.isGCPFunction = isGCPFunction - this.isAzureFunctionConsumptionPlan = isAzureFunctionConsumptionPlan - + this.isAzureFunctionConsumptionPlan = getIsAzureFunctionConsumptionPlan() this.spanLeakDebug = Number(DD_TRACE_SPAN_LEAK_DEBUG) - this.installSignature = { id: DD_INSTRUMENTATION_INSTALL_ID, time: DD_INSTRUMENTATION_INSTALL_TIME, @@ -697,6 +284,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) this._applyDefaults() this._applyEnvironment() this._applyOptions(options) + this._applyCalculated() this._applyRemote({}) this._merge() @@ -755,9 +343,19 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) this._applyOptions(options) } + // TODO: test + this._applyCalculated() this._merge() } + _isInServerlessEnvironment () { + const inAWSLambda = process.env.AWS_LAMBDA_FUNCTION_NAME !== undefined + const isGCPFunction = getIsGCPFunction() + const isAzureFunctionConsumptionPlan = getIsAzureFunctionConsumptionPlan() + return inAWSLambda || isGCPFunction || isAzureFunctionConsumptionPlan + } + + // for _merge to work, every config value must have a default value _applyDefaults () { const { AWS_LAMBDA_FUNCTION_NAME, @@ -775,27 +373,158 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) const defaults = this._defaults = {} - this._setValue(defaults, 'service', service) + this._setValue(defaults, 'appsec.blockedTemplateHtml', undefined) + this._setValue(defaults, 'appsec.blockedTemplateJson', undefined) + this._setValue(defaults, 'appsec.enabled', undefined) + this._setValue(defaults, 'appsec.obfuscatorKeyRegex', defaultWafObfuscatorKeyRegex) + this._setValue(defaults, 'appsec.obfuscatorValueRegex', defaultWafObfuscatorValueRegex) + this._setValue(defaults, 'appsec.rateLimit', 100) + this._setValue(defaults, 'appsec.rules', undefined) + this._setValue(defaults, 'appsec.wafTimeout', 5e3) // µs + this._setValue(defaults, 'clientIpEnabled', false) + this._setValue(defaults, 'clientIpHeader', null) + this._setValue(defaults, 'dbmPropagationMode', 'disabled') + this._setValue(defaults, 'dogstatsd.hostname', '127.0.0.1') + this._setValue(defaults, 'dogstatsd.port', '8125') + this._setValue(defaults, 'dsmEnabled', false) this._setValue(defaults, 'env', undefined) - this._setValue(defaults, 'version', pkg.version) - this._setUnit(defaults, 'sampleRate', undefined) - this._setBoolean(defaults, 'logInjection', false) - this._setArray(defaults, 'headerTags', []) + this._setValue(defaults, 'experimental.enableGetRumData', false) + this._setValue(defaults, 'experimental.exporter', undefined) + this._setValue(defaults, 'experimental.runtimeId', false) + this._setValue(defaults, 'flushInterval', 2000) + this._setValue(defaults, 'flushMinSpans', 1000) + this._setValue(defaults, 'gitMetadataEnabled', true) + this._setValue(defaults, 'headerTags', []) + this._setValue(defaults, 'hostname', '127.0.0.1') + this._setValue(defaults, 'iast.deduplicationEnabled', true) + this._setValue(defaults, 'iast.enabled', false) + this._setValue(defaults, 'iast.maxConcurrentRequests', 2) + this._setValue(defaults, 'iast.maxContextOperations', 2) + this._setValue(defaults, 'iast.redactionEnabled', true) + this._setValue(defaults, 'iast.redactionNamePattern', null) + this._setValue(defaults, 'iast.redactionValuePattern', null) + this._setValue(defaults, 'iast.requestSampling', 30) + this._setValue(defaults, 'iast.telemetryVerbosity', 'INFORMATION') + this._setValue(defaults, 'isCiVisibility', false) + this._setValue(defaults, 'isEarlyFlakeDetectionEnabled', false) + this._setValue(defaults, 'isGCPFunction', false) + this._setValue(defaults, 'isGitUploadEnabled', false) + this._setValue(defaults, 'isIntelligentTestRunnerEnabled', false) + this._setValue(defaults, 'isManualApiEnabled', false) + this._setValue(defaults, 'logInjection', false) + this._setValue(defaults, 'lookup', undefined) + this._setValue(defaults, 'openAiLogsEnabled', false) + this._setValue(defaults, 'openaiSpanCharLimit', 128) + this._setValue(defaults, 'peerServiceMapping', {}) + this._setValue(defaults, 'plugins', true) + this._setValue(defaults, 'port', '8126') + this._setValue(defaults, 'profiling.enabled', false) + this._setValue(defaults, 'profiling.exporters', 'agent') + this._setValue(defaults, 'profiling.sourceMap', true) + this._setValue(defaults, 'protocolVersion', '0.4') + this._setValue(defaults, 'queryStringObfuscation', qsRegex) + this._setValue(defaults, 'remoteConfig.enabled', true) + this._setValue(defaults, 'remoteConfig.pollInterval', 5) // seconds + this._setValue(defaults, 'reportHostname', false) + this._setValue(defaults, 'runtimeMetrics', false) + this._setValue(defaults, 'sampleRate', undefined) + this._setValue(defaults, 'sampler.rateLimit', undefined) + this._setValue(defaults, 'scope', undefined) + this._setValue(defaults, 'service', service) + this._setValue(defaults, 'site', 'datadoghq.com') + this._setValue(defaults, 'spanAttributeSchema', 'v0') + this._setValue(defaults, 'spanComputePeerService', false) + this._setValue(defaults, 'spanRemoveIntegrationFromService', false) + this._setValue(defaults, 'startupLogs', false) + this._setValue(defaults, 'stats.enabled', false) this._setValue(defaults, 'tags', {}) - this._setBoolean(defaults, 'tracing', true) + this._setValue(defaults, 'tagsHeaderMaxLength', 512) + this._setValue(defaults, 'telemetry.debug', false) + this._setValue(defaults, 'telemetry.dependencyCollection', true) + this._setValue(defaults, 'telemetry.enabled', true) + this._setValue(defaults, 'telemetry.heartbeatInterval', 60000) + this._setValue(defaults, 'telemetry.logCollection', false) + this._setValue(defaults, 'telemetry.metrics', true) + this._setValue(defaults, 'traceId128BitGenerationEnabled', true) + this._setValue(defaults, 'traceId128BitLoggingEnabled', false) + this._setValue(defaults, 'tracing', true) + this._setValue(defaults, 'url', undefined) + this._setValue(defaults, 'version', pkg.version) } _applyEnvironment () { const { + AWS_LAMBDA_FUNCTION_NAME, + DD_AGENT_HOST, + DD_APPSEC_ENABLED, + DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML, + DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON, + DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP, + DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP, + DD_APPSEC_RULES, + DD_APPSEC_TRACE_RATE_LIMIT, + DD_APPSEC_WAF_TIMEOUT, + DD_DATA_STREAMS_ENABLED, + DD_DBM_PROPAGATION_MODE, + DD_DOGSTATSD_HOSTNAME, + DD_DOGSTATSD_PORT, DD_ENV, + DD_EXPERIMENTAL_PROFILING_ENABLED, + JEST_WORKER_ID, + DD_IAST_DEDUPLICATION_ENABLED, + DD_IAST_ENABLED, + DD_IAST_MAX_CONCURRENT_REQUESTS, + DD_IAST_MAX_CONTEXT_OPERATIONS, + DD_IAST_REDACTION_ENABLED, + DD_IAST_REDACTION_NAME_PATTERN, + DD_IAST_REDACTION_VALUE_PATTERN, + DD_IAST_REQUEST_SAMPLING, + DD_IAST_TELEMETRY_VERBOSITY, + DD_INSTRUMENTATION_TELEMETRY_ENABLED, DD_LOGS_INJECTION, + DD_OPENAI_LOGS_ENABLED, + DD_OPENAI_SPAN_CHAR_LIMIT, + DD_PROFILING_ENABLED, + DD_PROFILING_EXPORTERS, + DD_PROFILING_SOURCE_MAP, + DD_REMOTE_CONFIGURATION_ENABLED, + DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS, + DD_RUNTIME_METRICS_ENABLED, DD_SERVICE, DD_SERVICE_NAME, + DD_SITE, DD_TAGS, + DD_TELEMETRY_DEBUG, + DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED, + DD_TELEMETRY_HEARTBEAT_INTERVAL, + DD_TELEMETRY_LOG_COLLECTION_ENABLED, + DD_TELEMETRY_METRICS_ENABLED, + DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED, + DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED, + DD_TRACE_AGENT_HOSTNAME, + DD_TRACE_AGENT_PORT, + DD_TRACE_AGENT_PROTOCOL_VERSION, + DD_TRACE_CLIENT_IP_ENABLED, + DD_TRACE_CLIENT_IP_HEADER, + DD_TRACE_EXPERIMENTAL_EXPORTER, + DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED, + DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED, + DD_TRACE_GIT_METADATA_ENABLED, DD_TRACE_GLOBAL_TAGS, DD_TRACE_HEADER_TAGS, + DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP, + DD_TRACE_PARTIAL_FLUSH_MIN_SPANS, + DD_TRACE_PEER_SERVICE_MAPPING, + DD_TRACE_RATE_LIMIT, + DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED, + DD_TRACE_REPORT_HOSTNAME, DD_TRACE_SAMPLE_RATE, + DD_TRACE_SCOPE, + DD_TRACE_SPAN_ATTRIBUTE_SCHEMA, + DD_TRACE_STARTUP_LOGS, DD_TRACE_TAGS, + DD_TRACE_TELEMETRY_ENABLED, + DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, DD_TRACING_ENABLED, DD_VERSION } = process.env @@ -807,31 +536,281 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) tagger.add(tags, DD_TRACE_TAGS) tagger.add(tags, DD_TRACE_GLOBAL_TAGS) - this._setString(env, 'service', DD_SERVICE || DD_SERVICE_NAME || tags.service) + this._setValue(env, 'appsec.blockedTemplateHtml', maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML)) + this._setValue(env, 'appsec.blockedTemplateJson', maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON)) + this._setBoolean(env, 'appsec.enabled', DD_APPSEC_ENABLED) + this._setString(env, 'appsec.obfuscatorKeyRegex', DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP) + this._setString(env, 'appsec.obfuscatorValueRegex', DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP) + this._setValue(env, 'appsec.rateLimit', maybeInt(DD_APPSEC_TRACE_RATE_LIMIT)) + this._setString(env, 'appsec.rules', DD_APPSEC_RULES) + this._setValue(env, 'appsec.wafTimeout', maybeInt(DD_APPSEC_WAF_TIMEOUT)) + this._setBoolean(env, 'clientIpEnabled', DD_TRACE_CLIENT_IP_ENABLED) + this._setString(env, 'clientIpHeader', DD_TRACE_CLIENT_IP_HEADER) + this._setString(env, 'dbmPropagationMode', DD_DBM_PROPAGATION_MODE) + this._setString(env, 'dogstatsd.hostname', DD_DOGSTATSD_HOSTNAME) + this._setString(env, 'dogstatsd.port', DD_DOGSTATSD_PORT) + this._setBoolean(env, 'dsmEnabled', DD_DATA_STREAMS_ENABLED) this._setString(env, 'env', DD_ENV || tags.env) - this._setString(env, 'version', DD_VERSION || tags.version) - this._setUnit(env, 'sampleRate', DD_TRACE_SAMPLE_RATE) - this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION) + this._setBoolean(env, 'experimental.enableGetRumData', DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED) + this._setString(env, 'experimental.exporter', DD_TRACE_EXPERIMENTAL_EXPORTER) + this._setBoolean(env, 'experimental.runtimeId', DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED) + if (AWS_LAMBDA_FUNCTION_NAME) this._setValue(env, 'flushInterval', 0) + this._setValue(env, 'flushMinSpans', maybeInt(DD_TRACE_PARTIAL_FLUSH_MIN_SPANS)) + this._setBoolean(env, 'gitMetadataEnabled', DD_TRACE_GIT_METADATA_ENABLED) this._setArray(env, 'headerTags', DD_TRACE_HEADER_TAGS) + this._setString(env, 'hostname', coalesce(DD_AGENT_HOST, DD_TRACE_AGENT_HOSTNAME)) + this._setBoolean(env, 'iast.deduplicationEnabled', DD_IAST_DEDUPLICATION_ENABLED) + this._setBoolean(env, 'iast.enabled', DD_IAST_ENABLED) + this._setValue(env, 'iast.maxConcurrentRequests', maybeInt(DD_IAST_MAX_CONCURRENT_REQUESTS)) + this._setValue(env, 'iast.maxContextOperations', maybeInt(DD_IAST_MAX_CONTEXT_OPERATIONS)) + this._setBoolean(env, 'iast.redactionEnabled', DD_IAST_REDACTION_ENABLED && !isFalse(DD_IAST_REDACTION_ENABLED)) + this._setString(env, 'iast.redactionNamePattern', DD_IAST_REDACTION_NAME_PATTERN) + this._setString(env, 'iast.redactionValuePattern', DD_IAST_REDACTION_VALUE_PATTERN) + const iastRequestSampling = maybeInt(DD_IAST_REQUEST_SAMPLING) + if (iastRequestSampling > -1 && iastRequestSampling < 101) { + this._setValue(env, 'iast.requestSampling', iastRequestSampling) + } + this._setString(env, 'iast.telemetryVerbosity', DD_IAST_TELEMETRY_VERBOSITY) + this._setBoolean(env, 'isGCPFunction', getIsGCPFunction()) + this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION) + this._setBoolean(env, 'openAiLogsEnabled', DD_OPENAI_LOGS_ENABLED) + this._setValue(env, 'openaiSpanCharLimit', maybeInt(DD_OPENAI_SPAN_CHAR_LIMIT)) + if (DD_TRACE_PEER_SERVICE_MAPPING) { + this._setValue(env, 'peerServiceMapping', fromEntries( + process.env.DD_TRACE_PEER_SERVICE_MAPPING.split(',').map(x => x.trim().split(':')) + )) + } + this._setString(env, 'port', DD_TRACE_AGENT_PORT) + this._setBoolean(env, 'profiling.enabled', coalesce(DD_EXPERIMENTAL_PROFILING_ENABLED, DD_PROFILING_ENABLED)) + this._setString(env, 'profiling.exporters', DD_PROFILING_EXPORTERS) + this._setBoolean(env, 'profiling.sourceMap', DD_PROFILING_SOURCE_MAP && !isFalse(DD_PROFILING_SOURCE_MAP)) + this._setString(env, 'protocolVersion', DD_TRACE_AGENT_PROTOCOL_VERSION) + this._setString(env, 'queryStringObfuscation', DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP) + this._setBoolean(env, 'remoteConfig.enabled', coalesce( + DD_REMOTE_CONFIGURATION_ENABLED, + !this._isInServerlessEnvironment() + )) + this._setValue(env, 'remoteConfig.pollInterval', maybeFloat(DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS)) + this._setBoolean(env, 'reportHostname', DD_TRACE_REPORT_HOSTNAME) + this._setBoolean(env, 'runtimeMetrics', DD_RUNTIME_METRICS_ENABLED) + this._setUnit(env, 'sampleRate', DD_TRACE_SAMPLE_RATE) + this._setValue(env, 'sampler.rateLimit', DD_TRACE_RATE_LIMIT) + this._setString(env, 'scope', DD_TRACE_SCOPE) + this._setString(env, 'service', DD_SERVICE || DD_SERVICE_NAME || tags.service) + this._setString(env, 'site', DD_SITE) + if (DD_TRACE_SPAN_ATTRIBUTE_SCHEMA) { + this._setString(env, 'spanAttributeSchema', validateNamingVersion(DD_TRACE_SPAN_ATTRIBUTE_SCHEMA)) + } + this._setBoolean(env, 'spanRemoveIntegrationFromService', DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED) + this._setBoolean(env, 'startupLogs', DD_TRACE_STARTUP_LOGS) this._setTags(env, 'tags', tags) + this._setValue(env, 'tagsHeaderMaxLength', DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH) + this._setBoolean(env, 'telemetry.enabled', coalesce( + DD_TRACE_TELEMETRY_ENABLED, // for backward compatibility + DD_INSTRUMENTATION_TELEMETRY_ENABLED, // to comply with instrumentation telemetry specs + !(this._isInServerlessEnvironment() || JEST_WORKER_ID) + )) + this._setBoolean(env, 'telemetry.debug', DD_TELEMETRY_DEBUG) + this._setBoolean(env, 'telemetry.dependencyCollection', DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED) + this._setValue(env, 'telemetry.heartbeatInterval', maybeInt(Math.floor(DD_TELEMETRY_HEARTBEAT_INTERVAL * 1000))) + this._setBoolean(env, 'telemetry.logCollection', coalesce(DD_TELEMETRY_LOG_COLLECTION_ENABLED, DD_IAST_ENABLED)) + this._setBoolean(env, 'telemetry.metrics', DD_TELEMETRY_METRICS_ENABLED) + this._setBoolean(env, 'traceId128BitGenerationEnabled', DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED) + this._setBoolean(env, 'traceId128BitLoggingEnabled', DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED) this._setBoolean(env, 'tracing', DD_TRACING_ENABLED) + this._setString(env, 'version', DD_VERSION || tags.version) } _applyOptions (options) { const opts = this._options = this._options || {} const tags = {} - options = Object.assign({ ingestion: {} }, options, opts) + options = this.options = Object.assign({ ingestion: {} }, options, opts) tagger.add(tags, options.tags) - this._setString(opts, 'service', options.service || tags.service) + this._setValue(opts, 'appsec.blockedTemplateHtml', maybeFile(options.appsec.blockedTemplateHtml)) + this._setValue(opts, 'appsec.blockedTemplateJson', maybeFile(options.appsec.blockedTemplateJson)) + this._setBoolean(opts, 'appsec.enabled', options.appsec.enabled) + this._setString(opts, 'appsec.obfuscatorKeyRegex', options.appsec.obfuscatorKeyRegex) + this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec.obfuscatorValueRegex) + this._setValue(opts, 'appsec.rateLimit', maybeInt(options.appsec.rateLimit)) + this._setString(opts, 'appsec.rules', options.appsec.rules) + this._setValue(opts, 'appsec.wafTimeout', maybeInt(options.appsec.wafTimeout)) + this._setBoolean(opts, 'clientIpEnabled', options.clientIpEnabled) + this._setString(opts, 'clientIpHeader', options.clientIpHeader) + this._setString(opts, 'dbmPropagationMode', options.dbmPropagationMode) + if (options.dogstatsd) { + this._setString(opts, 'dogstatsd.hostname', options.dogstatsd.hostname) + this._setString(opts, 'dogstatsd.port', options.dogstatsd.port) + } + this._setBoolean(opts, 'dsmEnabled', options.dsmEnabled) this._setString(opts, 'env', options.env || tags.env) - this._setString(opts, 'version', options.version || tags.version) - this._setUnit(opts, 'sampleRate', coalesce(options.sampleRate, options.ingestion.sampleRate)) - this._setBoolean(opts, 'logInjection', options.logInjection) + this._setBoolean(opts, 'experimental.enableGetRumData', + options.experimental && options.experimental.enableGetRumData) + this._setString(opts, 'experimental.exporter', options.experimental && options.experimental.exporter) + this._setBoolean(opts, 'experimental.runtimeId', options.experimental && options.experimental.runtimeId) + this._setValue(opts, 'flushInterval', maybeInt(options.flushInterval)) + this._setValue(opts, 'flushMinSpans', maybeInt(options.flushMinSpans)) this._setArray(opts, 'headerTags', options.headerTags) + this._setString(opts, 'hostname', options.hostname) + this._setBoolean(opts, 'iast.deduplicationEnabled', options.iastOptions && options.iastOptions.deduplicationEnabled) + this._setBoolean(opts, 'iast.enabled', + options.iastOptions && (options.iastOptions === true || options.iastOptions.enabled === true)) + const iastRequestSampling = maybeInt(options.iastOptions?.requestSampling) + this._setValue(opts, 'iast.maxConcurrentRequests', + maybeInt(options.iastOptions?.maxConcurrentRequests)) + this._setValue(opts, 'iast.maxContextOperations', + maybeInt(options.iastOptions && options.iastOptions.maxContextOperations)) + this._setBoolean(opts, 'iast.redactionEnabled', options.iastOptions && options.iastOptions.redactionEnabled) + this._setString(opts, 'iast.redactionNamePattern', options.iastOptions?.redactionNamePattern) + this._setString(opts, 'iast.redactionValuePattern', options.iastOptions?.redactionValuePattern) + if (iastRequestSampling > -1 && iastRequestSampling < 101) { + this._setValue(opts, 'iast.requestSampling', iastRequestSampling) + } + this._setString(opts, 'iast.telemetryVerbosity', options.iastOptions && options.iastOptions.telemetryVerbosity) + this._setBoolean(opts, 'isCiVisibility', options.isCiVisibility) + this._setBoolean(opts, 'logInjection', options.logInjection) + this._setString(opts, 'lookup', options.lookup) + this._setBoolean(opts, 'openAiLogsEnabled', options.openAiLogsEnabled) + this._setValue(opts, 'peerServiceMapping', options.peerServiceMapping) + this._setBoolean(opts, 'plugins', options.plugins) + this._setString(opts, 'port', options.port) + this._setBoolean(opts, 'profiling.enabled', options.profiling) + this._setString(opts, 'protocolVersion', options.protocolVersion) + if (options.remoteConfig) { + this._setValue(opts, 'remoteConfig.pollInterval', maybeFloat(options.remoteConfig.pollInterval)) + } + this._setBoolean(opts, 'reportHostname', options.reportHostname) + this._setBoolean(opts, 'runtimeMetrics', options.runtimeMetrics) + this._setUnit(opts, 'sampleRate', coalesce(options.sampleRate, options.ingestion.sampleRate)) + const ingestion = options.ingestion || {} + this._setValue(opts, 'sampler.rateLimit', coalesce(options.rateLimit, ingestion.rateLimit)) + this._setString(opts, 'service', options.service || tags.service) + this._setString(opts, 'site', options.site) + if (options.spanAttributeSchema) { + this._setString(opts, 'spanAttributeSchema', validateNamingVersion(options.spanAttributeSchema)) + } + this._setBoolean(opts, 'spanRemoveIntegrationFromService', options.spanRemoveIntegrationFromService) + this._setBoolean(opts, 'startupLogs', options.startupLogs) this._setTags(opts, 'tags', tags) + this._setBoolean(opts, 'telemetry.logCollection', options.iastOptions && + (options.iastOptions === true || options.iastOptions.enabled === true)) + this._setBoolean(opts, 'traceId128BitGenerationEnabled', options.traceId128BitGenerationEnabled) + this._setBoolean(opts, 'traceId128BitLoggingEnabled', options.traceId128BitLoggingEnabled) + this._setString(opts, 'version', options.version || tags.version) + } + + _isCiVisibility () { + return coalesce( + this.options.isCiVisibility, + this._defaults['isCiVisibility'] + ) + } + + _isCiVisibilityItrEnabled () { + return coalesce( + process.env.DD_CIVISIBILITY_ITR_ENABLED, + true + ) + } + + _getHostname () { + const DD_CIVISIBILITY_AGENTLESS_URL = process.env.DD_CIVISIBILITY_AGENTLESS_URL + const url = DD_CIVISIBILITY_AGENTLESS_URL ? new URL(DD_CIVISIBILITY_AGENTLESS_URL) + : getAgentUrl(this._getTraceAgentUrl(), this.options) + const DD_AGENT_HOST = coalesce( + this.options.hostname, + process.env.DD_AGENT_HOST, + process.env.DD_TRACE_AGENT_HOSTNAME, + '127.0.0.1' + ) + return DD_AGENT_HOST || (url && url.hostname) + } + + _getSpanComputePeerService () { + const DD_TRACE_SPAN_ATTRIBUTE_SCHEMA = validateNamingVersion( + coalesce( + this.options.spanAttributeSchema, + process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA + ) + ) + + const peerServiceSet = ( + this.options.hasOwnProperty('spanComputePeerService') || + process.env.hasOwnProperty('DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED') + ) + const peerServiceValue = coalesce( + this.options.spanComputePeerService, + process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED + ) + + const spanComputePeerService = ( + DD_TRACE_SPAN_ATTRIBUTE_SCHEMA === 'v0' + // In v0, peer service is computed only if it is explicitly set to true + ? peerServiceSet && isTrue(peerServiceValue) + // In >v0, peer service is false only if it is explicitly set to false + : (peerServiceSet ? !isFalse(peerServiceValue) : true) + ) + + return spanComputePeerService + } + + _isCiVisibilityGitUploadEnabled () { + return coalesce( + process.env.DD_CIVISIBILITY_GIT_UPLOAD_ENABLED, + true + ) + } + + _isCiVisibilityManualApiEnabled () { + return isTrue(coalesce( + process.env.DD_CIVISIBILITY_MANUAL_API_ENABLED, + false + )) + } + + _isTraceStatsComputationEnabled () { + return coalesce( + this.options.stats, + process.env.DD_TRACE_STATS_COMPUTATION_ENABLED, + getIsGCPFunction() || getIsAzureFunctionConsumptionPlan() + ) + } + + _getTraceAgentUrl () { + return coalesce( + this.options.url, + process.env.DD_TRACE_AGENT_URL, + process.env.DD_TRACE_URL, + null + ) + } + + // handles values calculated from a mixture of options and env vars + _applyCalculated () { + const calc = this._calculated = {} + + const { + DD_CIVISIBILITY_AGENTLESS_URL + } = process.env + + if (DD_CIVISIBILITY_AGENTLESS_URL) { + this._setValue(calc, 'url', new URL(DD_CIVISIBILITY_AGENTLESS_URL)) + } else { + this._setValue(calc, 'url', getAgentUrl(this._getTraceAgentUrl(), this.options)) + } + if (this._isCiVisibility()) { + this._setBoolean(calc, 'isEarlyFlakeDetectionEnabled', + coalesce(process.env.DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED, true)) + this._setBoolean(calc, 'isIntelligentTestRunnerEnabled', isTrue(this._isCiVisibilityItrEnabled())) + this._setBoolean(calc, 'isManualApiEnabled', this._isCiVisibilityManualApiEnabled()) + } + this._setString(calc, 'dogstatsd.hostname', this._getHostname()) + this._setBoolean(calc, 'isGitUploadEnabled', + calc['isIntelligentTestRunnerEnabled'] && !isFalse(this._isCiVisibilityGitUploadEnabled())) + this._setBoolean(calc, 'spanComputePeerService', this._getSpanComputePeerService()) + this._setBoolean(calc, 'stats.enabled', this._isTraceStatsComputationEnabled()) } _applyRemote (options) { @@ -890,7 +869,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) } _setString (obj, name, value) { - obj[name] = value || undefined // unset for empty strings + obj[name] = value ? String(value) : undefined // unset for empty strings } _setTags (obj, name, value) { @@ -908,9 +887,12 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) // TODO: Report origin changes and errors to telemetry. // TODO: Deeply merge configurations. // TODO: Move change tracking to telemetry. + // for telemetry reporting, `name`s in `containers` need to be keys from: + // eslint-disable-next-line max-len + // https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-payload/static/config_norm_rules.json _merge () { - const containers = [this._remote, this._options, this._env, this._defaults] - const origins = ['remote_config', 'code', 'env_var', 'default'] + const containers = [this._remote, this._options, this._env, this._calculated, this._defaults] + const origins = ['remote_config', 'code', 'env_var', 'calculated', 'default'] const changes = [] for (const name in this._defaults) { @@ -919,9 +901,10 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) const origin = origins[i] if ((container[name] !== null && container[name] !== undefined) || container === this._defaults) { - if (this[name] === container[name] && this.hasOwnProperty(name)) break + if (get(this, name) === container[name] && has(this, name)) break - const value = this[name] = container[name] + const value = container[name] + set(this, name, value) changes.push({ name, value, origin }) @@ -931,11 +914,19 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) } this.sampler.sampleRate = this.sampleRate - updateConfig(changes, this) } } +function maybeInt (number) { + const parsed = parseInt(number) + return isNaN(parsed) ? undefined : parsed +} +function maybeFloat (number) { + const parsed = parseFloat(number) + return isNaN(parsed) ? undefined : parsed +} + function getAgentUrl (url, options) { if (url) return new URL(url) diff --git a/packages/dd-trace/src/data_streams_context.js b/packages/dd-trace/src/data_streams_context.js index 94152fe11d9..33354920443 100644 --- a/packages/dd-trace/src/data_streams_context.js +++ b/packages/dd-trace/src/data_streams_context.js @@ -6,7 +6,7 @@ function getDataStreamsContext () { } function setDataStreamsContext (dataStreamsContext) { - storage.enterWith({ ...(storage.getStore()), dataStreamsContext }) + if (dataStreamsContext) storage.enterWith({ ...(storage.getStore()), dataStreamsContext }) } module.exports = { diff --git a/packages/dd-trace/src/datastreams/pathway.js b/packages/dd-trace/src/datastreams/pathway.js index 9865bfee7a7..52253f09ed7 100644 --- a/packages/dd-trace/src/datastreams/pathway.js +++ b/packages/dd-trace/src/datastreams/pathway.js @@ -8,6 +8,9 @@ const LRUCache = require('lru-cache') const options = { max: 500 } const cache = new LRUCache(options) +const CONTEXT_PROPAGATION_KEY = 'dd-pathway-ctx' +const CONTEXT_PROPAGATION_KEY_BASE64 = 'dd-pathway-ctx-base64' + function shaHash (checkpointString) { const hash = crypto.createHash('md5').update(checkpointString).digest('hex').slice(0, 16) return Buffer.from(hash, 'hex') @@ -33,6 +36,11 @@ function encodePathwayContext (dataStreamsContext) { ], 20) } +function encodePathwayContextBase64 (dataStreamsContext) { + const encodedPathway = encodePathwayContext(dataStreamsContext) + return encodedPathway.toString('base64') +} + function decodePathwayContext (pathwayContext) { if (pathwayContext == null || pathwayContext.length < 8) { return null @@ -51,8 +59,57 @@ function decodePathwayContext (pathwayContext) { return { hash: pathwayHash, pathwayStartNs: pathwayStartMs * 1e6, edgeStartNs: edgeStartMs * 1e6 } } +function decodePathwayContextBase64 (pathwayContext) { + if (pathwayContext == null || pathwayContext.length < 8) { + return + } + if (Buffer.isBuffer(pathwayContext)) { + pathwayContext = pathwayContext.toString() + } + const encodedPathway = Buffer.from(pathwayContext, 'base64') + return decodePathwayContext(encodedPathway) +} + +class DsmPathwayCodec { + // we use a class for encoding / decoding in case we update our encoding/decoding. A class will make updates easier + // instead of using individual functions. + static encode (dataStreamsContext, carrier) { + if (!dataStreamsContext || !dataStreamsContext.hash) { + return + } + carrier[CONTEXT_PROPAGATION_KEY_BASE64] = encodePathwayContextBase64(dataStreamsContext) + } + + static decode (carrier) { + if (carrier == null) return + + let ctx + if (CONTEXT_PROPAGATION_KEY_BASE64 in carrier) { + // decode v2 encoding of base64 + ctx = decodePathwayContextBase64(carrier[CONTEXT_PROPAGATION_KEY_BASE64]) + } else if (CONTEXT_PROPAGATION_KEY in carrier) { + try { + // decode v1 encoding + ctx = decodePathwayContext(carrier[CONTEXT_PROPAGATION_KEY]) + } catch { + // pass + } + // cover case where base64 context was received under wrong key + if (!ctx) ctx = decodePathwayContextBase64(carrier[CONTEXT_PROPAGATION_KEY]) + } + return ctx + } + + static contextExists (carrier) { + return CONTEXT_PROPAGATION_KEY_BASE64 in carrier || CONTEXT_PROPAGATION_KEY in carrier + } +} + module.exports = { computePathwayHash: computeHash, encodePathwayContext, - decodePathwayContext + decodePathwayContext, + encodePathwayContextBase64, + decodePathwayContextBase64, + DsmPathwayCodec } diff --git a/packages/dd-trace/src/datastreams/processor.js b/packages/dd-trace/src/datastreams/processor.js index f3e18ed9865..ca033bccc01 100644 --- a/packages/dd-trace/src/datastreams/processor.js +++ b/packages/dd-trace/src/datastreams/processor.js @@ -4,7 +4,7 @@ const pkg = require('../../../../package.json') const Uint64 = require('int64-buffer').Uint64BE const { LogCollapsingLowestDenseDDSketch } = require('@datadog/sketches-js') -const { encodePathwayContext } = require('./pathway') +const { DsmPathwayCodec } = require('./pathway') const { DataStreamsWriter } = require('./writer') const { computePathwayHash } = require('./pathway') const { types } = require('util') @@ -13,7 +13,6 @@ const { PATHWAY_HASH } = require('../../../../ext/tags') const ENTRY_PARENT_HASH = Buffer.from('0000000000000000', 'hex') const HIGH_ACCURACY_DISTRIBUTION = 0.0075 -const CONTEXT_PROPAGATION_KEY = 'dd-pathway-ctx' class StatsPoint { constructor (hash, parentHash, edgeTags) { @@ -285,7 +284,7 @@ class DataStreamsProcessor { // Add the header for this now, as the callee doesn't have access to context when producing // - 1 to account for extra byte for { const ddInfoContinued = {} - ddInfoContinued[CONTEXT_PROPAGATION_KEY] = encodePathwayContext(dataStreamsContext).toJSON() + DsmPathwayCodec.encode(dataStreamsContext, ddInfoContinued) payloadSize += getSizeOrZero(JSON.stringify(ddInfoContinued)) - 1 } const checkpoint = { @@ -364,6 +363,5 @@ module.exports = { getHeadersSize, getSizeOrZero, getAmqpMessageSize, - ENTRY_PARENT_HASH, - CONTEXT_PROPAGATION_KEY + ENTRY_PARENT_HASH } diff --git a/packages/dd-trace/src/format.js b/packages/dd-trace/src/format.js index d81ea7e5b21..6d7c85ce039 100644 --- a/packages/dd-trace/src/format.js +++ b/packages/dd-trace/src/format.js @@ -108,7 +108,6 @@ function extractTags (trace, span) { for (const tag in tags) { switch (tag) { - case 'operation.name': case 'service.name': case 'span.type': case 'resource.name': diff --git a/packages/dd-trace/src/opentracing/propagation/text_map.js b/packages/dd-trace/src/opentracing/propagation/text_map.js index 6c118406fe0..abe1835a0a2 100644 --- a/packages/dd-trace/src/opentracing/propagation/text_map.js +++ b/packages/dd-trace/src/opentracing/propagation/text_map.js @@ -1,6 +1,6 @@ 'use strict' -const pick = require('../../../../utils/src/pick') +const pick = require('../../../../datadog-core/src/utils/src/pick') const id = require('../../id') const DatadogSpanContext = require('../span_context') const log = require('../../log') diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 1c13592a136..cc2f3237898 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -58,6 +58,7 @@ const TEST_EARLY_FLAKE_IS_ENABLED = 'test.early_flake.is_enabled' const CI_APP_ORIGIN = 'ciapp-test' const JEST_TEST_RUNNER = 'test.jest.test_runner' +const JEST_DISPLAY_NAME = 'test.jest.display_name' const TEST_ITR_TESTS_SKIPPED = '_dd.ci.itr.tests_skipped' const TEST_ITR_SKIPPING_ENABLED = 'test.itr.tests_skipping.enabled' @@ -83,6 +84,7 @@ module.exports = { TEST_FRAMEWORK, TEST_FRAMEWORK_VERSION, JEST_TEST_RUNNER, + JEST_DISPLAY_NAME, TEST_TYPE, TEST_NAME, TEST_SUITE, diff --git a/packages/dd-trace/src/plugins/util/web.js b/packages/dd-trace/src/plugins/util/web.js index f145e27ef56..3e8bc787c72 100644 --- a/packages/dd-trace/src/plugins/util/web.js +++ b/packages/dd-trace/src/plugins/util/web.js @@ -1,6 +1,6 @@ 'use strict' -const uniq = require('../../../../utils/src/uniq') +const uniq = require('../../../../datadog-core/src/utils/src/uniq') const analyticsSampler = require('../../analytics_sampler') const FORMAT_HTTP_HEADERS = 'http_headers' const log = require('../../log') diff --git a/packages/dd-trace/src/profiling/exporters/agent.js b/packages/dd-trace/src/profiling/exporters/agent.js index 3ae99b5cb60..3ee4ae771be 100644 --- a/packages/dd-trace/src/profiling/exporters/agent.js +++ b/packages/dd-trace/src/profiling/exporters/agent.js @@ -9,6 +9,8 @@ const docker = require('../../exporters/common/docker') const FormData = require('../../exporters/common/form-data') const { storage } = require('../../../../datadog-core') const version = require('../../../../../package.json').version +const os = require('os') +const perf = require('perf_hooks').performance const containerId = docker.id() @@ -50,7 +52,7 @@ function computeRetries (uploadTimeout) { } class AgentExporter { - constructor ({ url, logger, uploadTimeout } = {}) { + constructor ({ url, logger, uploadTimeout, env, host, service, version } = {}) { this._url = url this._logger = logger @@ -58,6 +60,10 @@ class AgentExporter { this._backoffTime = backoffTime this._backoffTries = backoffTries + this._env = env + this._host = host + this._service = service + this._appVersion = version } export ({ profiles, start, end, tags }) { @@ -83,7 +89,37 @@ class AgentExporter { `profiler_version:${version}`, 'format:pprof', ...Object.entries(tags).map(([key, value]) => `${key}:${value}`) - ].join(',') + ].join(','), + info: { + application: { + env: this._env, + service: this._service, + start_time: new Date(perf.nodeTiming.nodeStart + perf.timeOrigin).toISOString(), + version: this._appVersion + }, + platform: { + hostname: this._host, + kernel_name: os.type(), + kernel_release: os.release(), + kernel_version: os.version() + }, + profiler: { + version + }, + runtime: { + // Using `nodejs` for consistency with the existing `runtime` tag. + // Note that the event `family` property uses `node`, as that's what's + // proscribed by the Intake API, but that's an internal enum and is + // not customer visible. + engine: 'nodejs', + // strip off leading 'v'. This makes the format consistent with other + // runtimes (e.g. Ruby) but not with the existing `runtime_version` tag. + // We'll keep it like this as we want cross-engine consistency. We + // also aren't changing the format of the existing tag as we don't want + // to break it. + version: process.version.substring(1) + } + } }) fields.push(['event', event, { diff --git a/packages/dd-trace/src/telemetry/index.js b/packages/dd-trace/src/telemetry/index.js index c03f6f4bda0..aa79ec7e23f 100644 --- a/packages/dd-trace/src/telemetry/index.js +++ b/packages/dd-trace/src/telemetry/index.js @@ -20,6 +20,7 @@ let heartbeatTimeout let heartbeatInterval let extendedInterval let integrations +let configWithOrigin = [] let retryData = null const extendedHeartbeatPayload = {} @@ -96,23 +97,6 @@ function getProducts (config) { return products } -function flatten (input, result = [], prefix = [], traversedObjects = null) { - traversedObjects = traversedObjects || new WeakSet() - if (traversedObjects.has(input)) { - return - } - traversedObjects.add(input) - for (const [key, value] of Object.entries(input)) { - if (typeof value === 'object' && value !== null) { - flatten(value, result, [...prefix, key], traversedObjects) - } else { - // TODO: add correct origin value - result.push({ name: [...prefix, key].join('.'), value, origin: 'unknown' }) - } - } - return result -} - function getInstallSignature (config) { const { installSignature: sig } = config if (sig && (sig.id || sig.time || sig.type)) { @@ -127,7 +111,7 @@ function getInstallSignature (config) { function appStarted (config) { const app = { products: getProducts(config), - configuration: flatten(config) + configuration: configWithOrigin } const installSignature = getInstallSignature(config) if (installSignature) { @@ -305,13 +289,18 @@ function updateIntegrations () { sendData(config, application, host, reqType, payload, updateRetryData) } +function formatMapForTelemetry (map) { + // format from an object to a string map in order for + // telemetry intake to accept the configuration + return map + ? Object.entries(map).map(([key, value]) => `${key}:${value}`).join(',') + : '' +} + function updateConfig (changes, config) { if (!config.telemetry.enabled) return if (changes.length === 0) return - // Hack to make system tests happy until we ship telemetry v2 - if (process.env.DD_INTERNAL_TELEMETRY_V2_ENABLED !== '1') return - const application = createAppObject(config) const host = createHostObject() @@ -325,24 +314,23 @@ function updateConfig (changes, config) { const configuration = [] for (const change of changes) { - if (!names.hasOwnProperty(change.name)) continue - - const name = names[change.name] + const name = names[change.name] || change.name const { origin, value } = change - const entry = { name, origin, value } + const entry = { name, value, origin } - if (Array.isArray(value)) { - entry.value = value.join(',') - } else if (name === 'DD_TAGS') { - entry.value = Object.entries(value).map(([key, value]) => `${key}:${value}`) - } + if (Array.isArray(value)) entry.value = value.join(',') + if (entry.name === 'DD_TAGS') entry.value = formatMapForTelemetry(entry.value) + if (entry.name === 'url' && entry.value) entry.value = entry.value.toString() + if (entry.name === 'peerServiceMapping' || entry.name === 'tags') entry.value = formatMapForTelemetry(entry.value) configuration.push(entry) } - - const { reqType, payload } = createPayload('app-client-configuration-change', { configuration }) - - sendData(config, application, host, reqType, payload, updateRetryData) + if (!configWithOrigin.length) { + configWithOrigin = configuration + } else { + const { reqType, payload } = createPayload('app-client-configuration-change', { configuration }) + sendData(config, application, host, reqType, payload, updateRetryData) + } } module.exports = { diff --git a/packages/dd-trace/src/tracer.js b/packages/dd-trace/src/tracer.js index 50859bf0deb..63c60e81440 100644 --- a/packages/dd-trace/src/tracer.js +++ b/packages/dd-trace/src/tracer.js @@ -8,7 +8,7 @@ const { isError } = require('./util') const { setStartupLogConfig } = require('./startup-log') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const { DataStreamsProcessor } = require('./datastreams/processor') -const { decodePathwayContext } = require('./datastreams/pathway') +const { DsmPathwayCodec } = require('./datastreams/pathway') const { DD_MAJOR } = require('../../../version') const DataStreamsContext = require('./data_streams_context') @@ -39,8 +39,8 @@ class DatadogTracer extends Tracer { return ctx } - decodeDataStreamsContext (data) { - const ctx = decodePathwayContext(data) + decodeDataStreamsContext (carrier) { + const ctx = DsmPathwayCodec.decode(carrier) // we erase the previous context everytime we decode a new one DataStreamsContext.setDataStreamsContext(ctx) return ctx diff --git a/packages/dd-trace/test/appsec/iast/index.spec.js b/packages/dd-trace/test/appsec/iast/index.spec.js index 803c8221d27..7035296d8de 100644 --- a/packages/dd-trace/test/appsec/iast/index.spec.js +++ b/packages/dd-trace/test/appsec/iast/index.spec.js @@ -143,9 +143,24 @@ describe('IAST Index', () => { }) it('should finish global context refresher on iast disabled', () => { + mockIast.enable(config) + mockIast.disable() expect(mockOverheadController.finishGlobalContext).to.have.been.calledOnce }) + + it('should start global context only once when calling enable multiple times', () => { + mockIast.enable(config) + mockIast.enable(config) + + expect(mockOverheadController.startGlobalContext).to.have.been.calledOnce + }) + + it('should not finish global context if not enabled before ', () => { + mockIast.disable(config) + + expect(mockOverheadController.finishGlobalContext).to.have.been.not.called + }) }) describe('managing vulnerability reporter', () => { @@ -156,6 +171,8 @@ describe('IAST Index', () => { }) it('should stop vulnerability reporter on iast disabled', () => { + mockIast.enable(config) + mockIast.disable() expect(mockVulnerabilityReporter.stop).to.have.been.calledOnce }) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter.spec.js index d822a417657..8371433df88 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter.spec.js @@ -13,12 +13,7 @@ describe('IAST Rewriter', () => { }) describe('Enabling rewriter', () => { - let rewriter, iastTelemetry - - const shimmer = { - wrap: sinon.spy(), - unwrap: sinon.spy() - } + let rewriter, iastTelemetry, shimmer class Rewriter { rewrite (content, filename) { @@ -35,28 +30,76 @@ describe('IAST Rewriter', () => { iastTelemetry = { add: sinon.spy() } + + shimmer = { + wrap: sinon.spy(), + unwrap: sinon.spy() + } + rewriter = proxyquire('../../../../src/appsec/iast/taint-tracking/rewriter', { - '@datadog/native-iast-rewriter': { Rewriter, getPrepareStackTrace: function () {} }, + '@datadog/native-iast-rewriter': { + Rewriter, + getPrepareStackTrace: function (fn) { + return function testWrappedPrepareStackTrace (_, callsites) { + return fn(_, callsites) + } + } + }, '../../../../../datadog-shimmer': shimmer, '../../telemetry': iastTelemetry }) }) afterEach(() => { - sinon.restore() + sinon.reset() }) it('Should wrap module compile method on taint tracking enable', () => { rewriter.enableRewriter() expect(shimmer.wrap).to.be.calledOnce expect(shimmer.wrap.getCall(0).args[1]).eq('_compile') + + rewriter.disableRewriter() }) it('Should unwrap module compile method on taint tracking disable', () => { rewriter.disableRewriter() + expect(shimmer.unwrap).to.be.calledOnce expect(shimmer.unwrap.getCall(0).args[1]).eq('_compile') }) + + it('Should keep original prepareStackTrace fn when calling enable and then disable', () => { + const orig = Error.prepareStackTrace + + rewriter.enableRewriter() + + const testPrepareStackTrace = (_, callsites) => { + // do nothing + } + Error.prepareStackTrace = testPrepareStackTrace + + rewriter.disableRewriter() + + expect(Error.prepareStackTrace).to.be.eq(testPrepareStackTrace) + + Error.prepareStackTrace = orig + }) + + it('Should keep original prepareStackTrace fn when calling disable only', () => { + const orig = Error.prepareStackTrace + + const testPrepareStackTrace = (_, callsites) => { + // do nothing + } + Error.prepareStackTrace = testPrepareStackTrace + + rewriter.disableRewriter() + + expect(Error.prepareStackTrace).to.be.eq(testPrepareStackTrace) + + Error.prepareStackTrace = orig + }) }) describe('getOriginalPathAndLineFromSourceMap', () => { diff --git a/packages/dd-trace/test/appsec/index.sequelize.plugin.spec.js b/packages/dd-trace/test/appsec/index.sequelize.plugin.spec.js new file mode 100644 index 00000000000..656532e883c --- /dev/null +++ b/packages/dd-trace/test/appsec/index.sequelize.plugin.spec.js @@ -0,0 +1,93 @@ +'use strict' + +const path = require('path') +const axios = require('axios') +const getPort = require('get-port') +const agent = require('../plugins/agent') +const appsec = require('../../src/appsec') +const Config = require('../../src/config') + +describe('sequelize', () => { + withVersions('sequelize', 'sequelize', sequelizeVersion => { + withVersions('mysql2', 'mysql2', () => { + withVersions('sequelize', 'express', (expressVersion) => { + let sequelize, User, server, port + + // init tracer + before(async () => { + await agent.load(['express', 'http'], { client: false }, { flushInterval: 1 }) + appsec.enable(new Config({ + appsec: { + enabled: true, + rules: path.join(__dirname, 'express-rules.json'), + apiSecurity: { + enabled: true, + requestSampling: 1 + } + } + })) + }) + + // close agent + after(() => { + appsec.disable() + return agent.close() + }) + + // init database + before(async () => { + const { Sequelize, DataTypes } = require(`../../../../versions/sequelize@${sequelizeVersion}`).get() + + sequelize = new Sequelize('db', 'root', '', { + host: '127.0.0.1', + dialect: 'mysql' + }) + User = sequelize.define('User', { + username: DataTypes.STRING, + birthday: DataTypes.DATE + }) + + await sequelize.sync({ force: true }) + await User.create({ + username: 'janedoe', + birthday: new Date(1980, 6, 20) + }) + }) + + // clean database + after(async () => { + await User.drop() + }) + + // init express + before((done) => { + const express = require(`../../../../versions/express@${expressVersion}`).get() + + const app = express() + app.get('/users', async (req, res) => { + const users = await User.findAll() + res.json(users) + }) + + getPort().then(newPort => { + port = newPort + server = app.listen(newPort, () => { + done() + }) + }) + }) + + // stop express + after(() => { + return server.close() + }) + + it('Should complete the request on time', (done) => { + axios.get(`http://localhost:${port}/users`) + .then(() => done()) + .catch(done) + }) + }) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/remote_config/index.spec.js b/packages/dd-trace/test/appsec/remote_config/index.spec.js index 9efc142c02e..217930c7f8e 100644 --- a/packages/dd-trace/test/appsec/remote_config/index.spec.js +++ b/packages/dd-trace/test/appsec/remote_config/index.spec.js @@ -253,7 +253,7 @@ describe('Remote Config index', () => { }) it('should not enable when custom appsec rules are provided', () => { - config.appsec = { enabled: true, rules: {}, customRulesProvided: true } + config.appsec = { enabled: true, rules: {} } remoteConfig.enable(config) remoteConfig.enableWafUpdate(config.appsec) @@ -262,7 +262,7 @@ describe('Remote Config index', () => { }) it('should enable when using default rules', () => { - config.appsec = { enabled: true, rules: {}, customRulesProvided: false } + config.appsec = { enabled: true, rules: null } remoteConfig.enable(config) remoteConfig.enableWafUpdate(config.appsec) diff --git a/packages/dd-trace/test/ci-visibility/exporters/ci-visibility-exporter.spec.js b/packages/dd-trace/test/ci-visibility/exporters/ci-visibility-exporter.spec.js index fcc773ecc58..36d8483fb0e 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/ci-visibility-exporter.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/ci-visibility-exporter.spec.js @@ -689,7 +689,12 @@ describe('CI Visibility Exporter', () => { .reply(200, JSON.stringify({ data: { attributes: { - test_full_names: ['suite1.test1', 'suite2.test2'] + tests: { + 'jest': { + 'suite1': ['test1'], + 'suite2': ['test2'] + } + } } } })) @@ -700,7 +705,12 @@ describe('CI Visibility Exporter', () => { ciVisibilityExporter._libraryConfig = { isEarlyFlakeDetectionEnabled: true } ciVisibilityExporter.getKnownTests({}, (err, knownTests) => { expect(err).to.be.null - expect(knownTests).to.eql(['suite1.test1', 'suite2.test2']) + expect(knownTests).to.eql({ + 'jest': { + 'suite1': ['test1'], + 'suite2': ['test2'] + } + }) expect(scope.isDone()).to.be.true done() }) @@ -727,7 +737,16 @@ describe('CI Visibility Exporter', () => { requestHeaders = this.req.headers return zlib.gzipSync(JSON.stringify({ - data: { attributes: { test_full_names: ['suite1.test1', 'suite2.test2'] } } + data: { + attributes: { + tests: { + 'jest': { + 'suite1': ['test1'], + 'suite2': ['test2'] + } + } + } + } })) }, { 'content-encoding': 'gzip' @@ -740,7 +759,12 @@ describe('CI Visibility Exporter', () => { ciVisibilityExporter._isGzipCompatible = true ciVisibilityExporter.getKnownTests({}, (err, knownTests) => { expect(err).to.be.null - expect(knownTests).to.eql(['suite1.test1', 'suite2.test2']) + expect(knownTests).to.eql({ + 'jest': { + 'suite1': ['test1'], + 'suite2': ['test2'] + } + }) expect(scope.isDone()).to.be.true expect(requestHeaders['accept-encoding']).to.equal('gzip') done() @@ -754,7 +778,16 @@ describe('CI Visibility Exporter', () => { requestHeaders = this.req.headers return JSON.stringify({ - data: { attributes: { test_full_names: ['suite1.test1', 'suite2.test2'] } } + data: { + attributes: { + tests: { + 'jest': { + 'suite1': ['test1'], + 'suite2': ['test2'] + } + } + } + } }) }) @@ -767,7 +800,12 @@ describe('CI Visibility Exporter', () => { ciVisibilityExporter.getKnownTests({}, (err, knownTests) => { expect(err).to.be.null - expect(knownTests).to.eql(['suite1.test1', 'suite2.test2']) + expect(knownTests).to.eql({ + 'jest': { + 'suite1': ['test1'], + 'suite2': ['test2'] + } + }) expect(scope.isDone()).to.be.true expect(requestHeaders['accept-encoding']).not.to.equal('gzip') done() diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 500ab936d36..e7792ab7a36 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -4,6 +4,7 @@ require('./setup/tap') const { expect } = require('chai') const { readFileSync } = require('fs') +const sinon = require('sinon') describe('Config', () => { let Config @@ -15,6 +16,7 @@ describe('Config', () => { let existsSyncParam let existsSyncReturn let osType + let updateConfig const RECOMMENDED_JSON_PATH = require.resolve('../src/appsec/recommended.json') const RULES_JSON_PATH = require.resolve('./fixtures/config/appsec-rules.json') @@ -39,6 +41,8 @@ describe('Config', () => { error: sinon.spy() } + updateConfig = sinon.stub() + env = process.env process.env = {} fs = { @@ -57,12 +61,14 @@ describe('Config', () => { Config = proxyquire('../src/config', { './pkg': pkg, './log': log, + './telemetry': { updateConfig }, fs, os }) }) afterEach(() => { + updateConfig.reset() process.env = env existsSyncParam = undefined }) @@ -102,7 +108,6 @@ describe('Config', () => { expect(config).to.have.nested.property('experimental.enableGetRumData', false) expect(config).to.have.nested.property('appsec.enabled', undefined) expect(config).to.have.nested.property('appsec.rules', undefined) - expect(config).to.have.nested.property('appsec.customRulesProvided', false) expect(config).to.have.nested.property('appsec.rateLimit', 100) expect(config).to.have.nested.property('appsec.wafTimeout', 5e3) expect(config).to.have.nested.property('appsec.obfuscatorKeyRegex').with.length(155) @@ -124,6 +129,77 @@ describe('Config', () => { expect(config).to.have.nested.property('installSignature.id', null) expect(config).to.have.nested.property('installSignature.time', null) expect(config).to.have.nested.property('installSignature.type', null) + + expect(updateConfig).to.be.calledOnce + + expect(updateConfig.getCall(0).args[0]).to.deep.include( + { name: 'service', value: 'node', origin: 'default' }, + { name: 'logInjection', value: false, origin: 'default' }, + { name: 'headerTags', value: [], origin: 'default' }, + { name: 'tracing', value: true, origin: 'default' }, + { name: 'dbmPropagationMode', value: 'disabled', origin: 'default' }, + { name: 'dsmEnabled', value: false, origin: 'default' }, + { name: 'openAiLogsEnabled', value: false, origin: 'default' }, + { name: 'url', value: undefined, origin: 'default' }, + { name: 'site', value: 'datadoghq.com', origin: 'default' }, + { name: 'hostname', value: '127.0.0.1', origin: 'default' }, + { name: 'port', value: '8126', origin: 'default' }, + { name: 'debug', value: false, origin: 'default' }, + { name: 'protocolVersion', value: '0.4', origin: 'default' }, + { name: 'dogstatsd.port', value: '8125', origin: 'default' }, + { name: 'flushInterval', value: 2000, origin: 'default' }, + { name: 'flushMinSpans', value: 1000, origin: 'default' }, + { name: 'clientIpEnabled', value: false, origin: 'default' }, + { name: 'clientIpHeader', value: null, origin: 'default' }, + { name: 'sampleRate', value: undefined, origin: 'default' }, + { name: 'runtimeMetrics', value: false, origin: 'default' }, + { name: 'plugins', value: true, origin: 'default' }, + { name: 'reportHostname', value: false, origin: 'default' }, + { name: 'scope', value: undefined, origin: 'default' }, + { name: 'logLevel', value: 'debug', origin: 'default' }, + { name: 'traceId128BitGenerationEnabled', value: false, origin: 'default' }, + { name: 'traceId128BitLoggingEnabled', value: false, origin: 'default' }, + { name: 'spanAttributeSchema', value: 'v0', origin: 'default' }, + { name: 'spanRemoveIntegrationFromService', value: false, origin: 'default' }, + { name: 'peerServiceMapping', value: '', origin: 'default' }, + { name: 'tracePropagationStyle.extract', value: ['datadog', 'tracecontext'], origin: 'default' }, + { name: 'experimental.runtimeId', value: false, origin: 'default' }, + { name: 'experimental.exporter', value: undefined, origin: 'default' }, + { name: 'experimental.enableGetRumData', value: false, origin: 'default' }, + { name: 'reportHostname', value: false, origin: 'default' }, + { name: 'profiling.enabled', value: false, origin: 'default' }, + { name: 'profiling.sourceMap', value: true, origin: 'default' }, + { name: 'profiling.exporters', value: 'agent', origin: 'default' }, + { name: 'startupLogs', value: false, origin: 'default' }, + { name: 'telemetry.enabled', value: true, origin: 'default' }, + { name: 'telemetry.heartbeatInterval', value: 60000, origin: 'default' }, + { name: 'telemetry.debug', value: false, origin: 'default' }, + { name: 'telemetry.metrics', value: false, origin: 'default' }, + { name: 'telemetry.dependencyCollection', value: true, origin: 'default' }, + { name: 'tagsHeaderMaxLength', value: 512, origin: 'default' }, + { name: 'appsec.enabled', value: undefined, origin: 'default' }, + { name: 'appsec.rules', value: undefined, origin: 'default' }, + { name: 'appsec.rateLimit', value: 100, origin: 'default' }, + { name: 'appsec.wafTimeout', value: 5e3, origin: 'default' }, + { name: 'appsec.blockedTemplateHtml', value: undefined, origin: 'default' }, + { name: 'appsec.blockedTemplateJson', value: undefined, origin: 'default' }, + { name: 'appsec.eventTracking.enabled', value: true, origin: 'default' }, + { name: 'appsec.eventTracking.mode', value: 'safe', origin: 'default' }, + { name: 'remoteConfig.enabled', value: true, origin: 'default' }, + { name: 'remoteConfig.pollInterval', value: 5, origin: 'default' }, + { name: 'iast.enabled', value: false, origin: 'default' }, + { name: 'iast.requestSampling', value: 30, origin: 'default' }, + { name: 'iast.maxConcurrentRequests', value: 2, origin: 'default' }, + { name: 'iast.maxContextOperations', value: 2, origin: 'default' }, + { name: 'iast.deduplicationEnabled', value: true, origin: 'default' }, + { name: 'iast.redactionEnabled', value: true, origin: 'default' }, + { name: 'iast.telemetryVerbosity', value: 'INFORMATION', origin: 'default' }, + { name: 'isCiVisibility', value: false, origin: 'default' }, + { name: 'gitMetadataEnabled', value: true, origin: 'default' }, + { name: 'openaiSpanCharLimit', value: 128, origin: 'default' }, + { name: 'traceId128BitGenerationEnabled', value: false, origin: 'default' }, + { name: 'traceId128BitLoggingEnabled', value: false, origin: 'default' } + ) }) it('should support logging', () => { @@ -291,7 +367,6 @@ describe('Config', () => { expect(config).to.have.nested.property('experimental.enableGetRumData', true) expect(config).to.have.nested.property('appsec.enabled', true) expect(config).to.have.nested.property('appsec.rules', RULES_JSON_PATH) - expect(config).to.have.nested.property('appsec.customRulesProvided', true) expect(config).to.have.nested.property('appsec.rateLimit', 42) expect(config).to.have.nested.property('appsec.wafTimeout', 42) expect(config).to.have.nested.property('appsec.obfuscatorKeyRegex', '.*') @@ -319,6 +394,53 @@ describe('Config', () => { type: 'k8s_single_step', time: '1703188212' }) + + expect(updateConfig).to.be.calledOnce + + expect(updateConfig.getCall(0).args[0]).to.deep.include( + { name: 'tracing', value: false, origin: 'env_var' }, + { name: 'debug', value: true, origin: 'env_var' }, + { name: 'protocolVersion', value: '0.5', origin: 'env_var' }, + { name: 'hostname', value: 'agent', origin: 'env_var' }, + { name: 'dogstatsd.hostname', value: 'dsd-agent', origin: 'env_var' }, + { name: 'dogstatsd.port', value: '5218', origin: 'env_var' }, + { name: 'service', value: 'service', origin: 'env_var' }, + { name: 'version', value: '1.0.0', origin: 'env_var' }, + { name: 'clientIpEnabled', value: true, origin: 'env_var' }, + { name: 'clientIpHeader', value: 'x-true-client-ip', origin: 'env_var' }, + { name: 'runtimeMetrics', value: true, origin: 'env_var' }, + { name: 'reportHostname', value: true, origin: 'env_var' }, + { name: 'env', value: 'test', origin: 'env_var' }, + { name: 'sampleRate', value: 0.5, origin: 'env_var' }, + { name: 'traceId128BitGenerationEnabled', value: true, origin: 'env_var' }, + { name: 'traceId128BitLoggingEnabled', value: true, origin: 'env_var' }, + { name: 'spanAttributeSchema', value: 'v1', origin: 'env_var' }, + { name: 'spanComputePeerService', value: true, origin: 'env_var' }, + { name: 'sampler.rateLimit', value: '-1', origin: 'env_var' }, + { name: 'spanRemoveIntegrationFromService', value: true, origin: 'env_var' }, + { name: 'peerServiceMapping', value: 'c:cc,d:dd', origin: 'env_var' }, + { name: 'tracePropagationStyle.extract', value: ['b3', 'tracecontext'], origin: 'env_var' }, + { name: 'experimental.runtimeId', value: true, origin: 'env_var' }, + { name: 'experimental.exporter', value: 'log', origin: 'env_var' }, + { name: 'experimental.enableGetRumData', value: true, origin: 'env_var' }, + { name: 'appsec.enabled', value: true, origin: 'env_var' }, + { name: 'appsec.rules', value: RULES_JSON_PATH, origin: 'env_var' }, + { name: 'appsec.rateLimit', value: 42, origin: 'env_var' }, + { name: 'appsec.wafTimeout', value: 42, origin: 'env_var' }, + { name: 'appsec.blockedTemplateHtml', value: BLOCKED_TEMPLATE_HTML, origin: 'env_var' }, + { name: 'appsec.blockedTemplateJson', value: BLOCKED_TEMPLATE_JSON, origin: 'env_var' }, + { name: 'appsec.eventTracking.enabled', value: true, origin: 'env_var' }, + { name: 'appsec.eventTracking.mode', value: 'extended', origin: 'env_var' }, + { name: 'remoteConfig.enabled', value: false, origin: 'calculated' }, + { name: 'remoteConfig.pollInterval', value: 42, origin: 'env_var' }, + { name: 'iast.enabled', value: true, origin: 'env_var' }, + { name: 'iast.requestSampling', value: 40, origin: 'env_var' }, + { name: 'iast.maxConcurrentRequests', value: 3, origin: 'env_var' }, + { name: 'iast.maxContextOperations', value: 4, origin: 'env_var' }, + { name: 'iast.deduplicationEnabled', value: false, origin: 'env_var' }, + { name: 'iast.redactionEnabled', value: false, origin: 'env_var' }, + { name: 'iast.telemetryVerbosity', value: 'DEBUG', origin: 'env_var' } + ) }) it('should ignore empty strings', () => { @@ -529,6 +651,48 @@ describe('Config', () => { a: 'aa', b: 'bb' }) + + expect(updateConfig).to.be.calledOnce + + expect(updateConfig.getCall(0).args[0]).to.deep.include( + { name: 'protocolVersion', value: '0.5', origin: 'code' }, + { name: 'site', value: 'datadoghq.eu', origin: 'code' }, + { name: 'hostname', value: 'agent', origin: 'code' }, + { name: 'port', value: '6218', origin: 'code' }, + { name: 'dogstatsd.hostname', value: 'agent-dsd', origin: 'calculated' }, + { name: 'dogstatsd.port', value: '5218', origin: 'code' }, + { name: 'service', value: 'service', origin: 'code' }, + { name: 'version', value: '0.1.0', origin: 'code' }, + { name: 'env', value: 'test', origin: 'code' }, + { name: 'sampleRate', value: 0.5, origin: 'code' }, + { name: 'clientIpEnabled', value: true, origin: 'code' }, + { name: 'clientIpHeader', value: 'x-true-client-ip', origin: 'code' }, + { name: 'flushInterval', value: 5000, origin: 'code' }, + { name: 'flushMinSpans', value: 500, origin: 'code' }, + { name: 'runtimeMetrics', value: true, origin: 'code' }, + { name: 'reportHostname', value: true, origin: 'code' }, + { name: 'plugins', value: false, origin: 'code' }, + { name: 'logLevel', value: logLevel, origin: 'code' }, + { name: 'traceId128BitGenerationEnabled', value: true, origin: 'code' }, + { name: 'traceId128BitLoggingEnabled', value: true, origin: 'code' }, + { name: 'spanRemoveIntegrationFromService', value: true, origin: 'code' }, + { name: 'spanComputePeerService', value: true, origin: 'code' }, + { name: 'peerServiceMapping', value: 'd:dd', origin: 'code' }, + { name: 'tracePropagationStyle.extract', value: ['datadog'], origin: 'calculated' }, + { name: 'experimental.runtimeId', value: true, origin: 'code' }, + { name: 'experimental.exporter', value: 'log', origin: 'code' }, + { name: 'experimental.enableGetRumData', value: true, origin: 'code' }, + { name: 'appsec.enabled', value: false, origin: 'code' }, + { name: 'remoteConfig.pollInterval', value: 42, origin: 'code' }, + { name: 'iast.enabled', value: true, origin: 'code' }, + { name: 'iast.requestSampling', value: 50, origin: 'code' }, + { name: 'iast.maxConcurrentRequests', value: 4, origin: 'code' }, + { name: 'iast.maxContextOperations', value: 5, origin: 'code' }, + { name: 'iast.deduplicationEnabled', value: false, origin: 'code' }, + { name: 'iast.redactionEnabled', value: false, origin: 'code' }, + { name: 'iast.telemetryVerbosity', value: 'DEBUG', origin: 'code' }, + { name: 'sampler.sampleRate', value: 0.5, origin: 'code' } + ) }) it('should initialize from the options with url taking precedence', () => { @@ -798,7 +962,6 @@ describe('Config', () => { expect(config).to.have.nested.property('experimental.enableGetRumData', false) expect(config).to.have.nested.property('appsec.enabled', true) expect(config).to.have.nested.property('appsec.rules', RULES_JSON_PATH) - expect(config).to.have.nested.property('appsec.customRulesProvided', true) expect(config).to.have.nested.property('appsec.rateLimit', 42) expect(config).to.have.nested.property('appsec.wafTimeout', 42) expect(config).to.have.nested.property('appsec.obfuscatorKeyRegex', '.*') @@ -866,7 +1029,6 @@ describe('Config', () => { expect(config).to.have.deep.property('appsec', { enabled: true, rules: undefined, - customRulesProvided: false, rateLimit: 42, wafTimeout: 42, obfuscatorKeyRegex: '.*', @@ -1113,6 +1275,15 @@ describe('Config', () => { expect(config.remoteConfig.enabled).to.be.false }) + it('should send empty array when remote config is called on empty options', () => { + const config = new Config() + + config.configure({}, true) + + expect(updateConfig).to.be.calledTwice + expect(updateConfig.getCall(1).args[0]).to.deep.equal([]) + }) + it('should ignore invalid iast.requestSampling', () => { const config = new Config({ experimental: { @@ -1166,7 +1337,6 @@ describe('Config', () => { expect(config.appsec.enabled).to.be.true expect(config.appsec.rules).to.eq('path/to/rules.json') - expect(config.appsec.customRulesProvided).to.be.true expect(config.appsec.blockedTemplateHtml).to.be.undefined expect(config.appsec.blockedTemplateJson).to.be.undefined expect(config.appsec.blockedTemplateGraphql).to.be.undefined diff --git a/packages/dd-trace/test/datastreams/pathway.spec.js b/packages/dd-trace/test/datastreams/pathway.spec.js index 0722f220f69..208383a1c0f 100644 --- a/packages/dd-trace/test/datastreams/pathway.spec.js +++ b/packages/dd-trace/test/datastreams/pathway.spec.js @@ -3,7 +3,14 @@ require('../setup/tap') const { expect } = require('chai') -const { computePathwayHash, encodePathwayContext, decodePathwayContext } = require('../../src/datastreams/pathway') +const { + computePathwayHash, + encodePathwayContext, + decodePathwayContext, + encodePathwayContextBase64, + decodePathwayContextBase64, + DsmPathwayCodec +} = require('../../src/datastreams/pathway') describe('encoding', () => { it('hash should always give the same value', () => { @@ -40,4 +47,103 @@ describe('encoding', () => { expect(decoded.pathwayStartNs).to.equal(expectedContext.pathwayStartNs) expect(decoded.edgeStartNs).to.equal(expectedContext.edgeStartNs) }) + it('should encode and decode to the same value when using base64', () => { + const ctx = { + pathwayStartNs: 1685673482722000000, + edgeStartNs: 1685673506404000000 + } + ctx.hash = computePathwayHash('test-service', 'test-env', + ['direction:in', 'group:group1', 'topic:topic1', 'type:kafka'], Buffer.from('0000000000000000', 'hex')) + + const encodedPathway = encodePathwayContextBase64(ctx) + const decodedPathway = decodePathwayContextBase64(encodedPathway) + + expect(decodedPathway.hash.toString()).to.equal(ctx.hash.toString()) + expect(decodedPathway.pathwayStartNs).to.equal(ctx.pathwayStartNs) + expect(decodedPathway.edgeStartNs).to.equal(ctx.edgeStartNs) + }) + + it('should encode and decode to the same value when using the PathwayCodec', () => { + const ctx = { + pathwayStartNs: 1685673482722000000, + edgeStartNs: 1685673506404000000 + } + const carrier = {} + ctx.hash = computePathwayHash('test-service', 'test-env', + ['direction:in', 'group:group1', 'topic:topic1', 'type:kafka'], Buffer.from('0000000000000000', 'hex')) + + DsmPathwayCodec.encode(ctx, carrier) + const decodedCtx = DsmPathwayCodec.decode(carrier) + + expect(decodedCtx.hash.toString()).to.equal(ctx.hash.toString()) + expect(decodedCtx.pathwayStartNs).to.equal(ctx.pathwayStartNs) + expect(decodedCtx.edgeStartNs).to.equal(ctx.edgeStartNs) + }) + + it('should encode/decode to the same value when using the PathwayCodec, base64 and the deprecated ctx key', () => { + const ctx = { + pathwayStartNs: 1685673482722000000, + edgeStartNs: 1685673506404000000 + } + const carrier = {} + ctx.hash = computePathwayHash('test-service', 'test-env', + ['direction:in', 'group:group1', 'topic:topic1', 'type:kafka'], Buffer.from('0000000000000000', 'hex')) + + DsmPathwayCodec.encode(ctx, carrier) + carrier['dd-pathway-ctx'] = carrier['dd-pathway-ctx-base64'] + delete carrier['dd-pathway-ctx-base64'] + const decodedCtx = DsmPathwayCodec.decode(carrier) + + expect(decodedCtx.hash.toString()).to.equal(ctx.hash.toString()) + expect(decodedCtx.pathwayStartNs).to.equal(ctx.pathwayStartNs) + expect(decodedCtx.edgeStartNs).to.equal(ctx.edgeStartNs) + }) + + it('should encode/decode to the same value when using the PathwayCodec and the deprecated encoding', () => { + const ctx = { + pathwayStartNs: 1685673482722000000, + edgeStartNs: 1685673506404000000 + } + const carrier = {} + ctx.hash = computePathwayHash('test-service', 'test-env', + ['direction:in', 'group:group1', 'topic:topic1', 'type:kafka'], Buffer.from('0000000000000000', 'hex')) + + carrier['dd-pathway-ctx'] = encodePathwayContext(ctx) + const decodedCtx = DsmPathwayCodec.decode(carrier) + + expect(decodedCtx.hash.toString()).to.equal(ctx.hash.toString()) + expect(decodedCtx.pathwayStartNs).to.equal(ctx.pathwayStartNs) + expect(decodedCtx.edgeStartNs).to.equal(ctx.edgeStartNs) + }) + + it('should inject the base64 encoded string to the carrier', () => { + const ctx = { + pathwayStartNs: 1685673482722000000, + edgeStartNs: 1685673506404000000 + } + const carrier = {} + ctx.hash = computePathwayHash('test-service', 'test-env', + ['direction:in', 'group:group1', 'topic:topic1', 'type:kafka'], Buffer.from('0000000000000000', 'hex')) + + DsmPathwayCodec.encode(ctx, carrier) + + const expectedBase64Hash = '7Jnh6OaCmF3E58Cfj2LI2cOfj2I=' + expect(carrier['dd-pathway-ctx-base64']).to.equal(expectedBase64Hash) + }) + + it('should extract the base64 encoded string from the carrier', () => { + const ctx = { + pathwayStartNs: 1685673482722000000, + edgeStartNs: 1685673506404000000 + } + ctx.hash = computePathwayHash('test-service', 'test-env', + ['direction:in', 'group:group1', 'topic:topic1', 'type:kafka'], Buffer.from('0000000000000000', 'hex')) + + const carrier = {} + const expectedBase64Hash = '7Jnh6OaCmF3E58Cfj2LI2cOfj2I=' + carrier['dd-pathway-ctx-base64'] = expectedBase64Hash + const decodedCtx = DsmPathwayCodec.decode(carrier) + + expect(decodedCtx.hash.toString()).to.equal(ctx.hash.toString()) + }) }) diff --git a/packages/dd-trace/test/format.spec.js b/packages/dd-trace/test/format.spec.js index 516fbb25c33..37306d46b15 100644 --- a/packages/dd-trace/test/format.spec.js +++ b/packages/dd-trace/test/format.spec.js @@ -142,14 +142,12 @@ describe('format', () => { }) it('should extract Datadog specific tags', () => { - spanContext._tags['operation.name'] = 'name' spanContext._tags['service.name'] = 'service' spanContext._tags['span.type'] = 'type' spanContext._tags['resource.name'] = 'resource' trace = format(span) - expect(trace.name).to.equal('name') expect(trace.service).to.equal('service') expect(trace.type).to.equal('type') expect(trace.resource).to.equal('resource') diff --git a/packages/dd-trace/test/plugins/externals.json b/packages/dd-trace/test/plugins/externals.json index fef58f5dfd5..3e6a39a0043 100644 --- a/packages/dd-trace/test/plugins/externals.json +++ b/packages/dd-trace/test/plugins/externals.json @@ -251,6 +251,10 @@ { "name": "bson", "versions": ["4.0.0"] + }, + { + "name": "mongodb", + "versions": ["6.3.0"] } ], "mongoose": [ @@ -325,5 +329,11 @@ "name": "redis", "versions": ["^4"] } + ], + "sequelize": [ + { + "name": "express", + "versions": [">=4"] + } ] } diff --git a/packages/dd-trace/test/profiling/exporters/agent.spec.js b/packages/dd-trace/test/profiling/exporters/agent.spec.js index ea6210632ee..88f3fa54886 100644 --- a/packages/dd-trace/test/profiling/exporters/agent.spec.js +++ b/packages/dd-trace/test/profiling/exporters/agent.spec.js @@ -100,6 +100,26 @@ describe('exporters/agent', function () { 'format:pprof', `runtime-id:${RUNTIME_ID}` ].join(',')) + expect(event).to.have.property('info') + expect(event.info).to.have.property('application') + expect(Object.keys(event.info.application)).to.have.length(4) + expect(event.info.application).to.have.property('env', ENV) + expect(event.info.application).to.have.property('service', SERVICE) + expect(event.info.application).to.have.property('start_time') + expect(event.info.application).to.have.property('version', '1.2.3') + expect(event.info).to.have.property('platform') + expect(Object.keys(event.info.platform)).to.have.length(4) + expect(event.info.platform).to.have.property('hostname', HOST) + expect(event.info.platform).to.have.property('kernel_name', os.type()) + expect(event.info.platform).to.have.property('kernel_release', os.release()) + expect(event.info.platform).to.have.property('kernel_version', os.version()) + expect(event.info).to.have.property('profiler') + expect(Object.keys(event.info.profiler)).to.have.length(1) + expect(event.info.profiler).to.have.property('version', version) + expect(event.info).to.have.property('runtime') + expect(Object.keys(event.info.runtime)).to.have.length(2) + expect(event.info.runtime).to.have.property('engine', 'nodejs') + expect(event.info.runtime).to.have.property('version', process.version.substring(1)) expect(req.files[1]).to.have.property('fieldname', 'wall.pprof') expect(req.files[1]).to.have.property('originalname', 'wall.pprof') diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index 3e83bd24e98..2be25250dda 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -1,21 +1,16 @@ 'use strict' -/* eslint-disable no-console */ - require('./core') const os = require('os') const path = require('path') const semver = require('semver') const externals = require('../plugins/externals.json') -const slackReport = require('./slack-report') const runtimeMetrics = require('../../src/runtime_metrics') const agent = require('../plugins/agent') const Nomenclature = require('../../src/service-naming') const { storage } = require('../../../datadog-core') const { schemaDefinitions } = require('../../src/service-naming/schemas') -const mochaVersion = require('mocha/package.json').version -console.log('MOCHA VERSION', mochaVersion) global.withVersions = withVersions global.withExports = withExports @@ -24,8 +19,6 @@ global.withPeerService = withPeerService const testedPlugins = agent.testedPlugins -const packageVersionFailures = Object.create({}) - function loadInst (plugin) { const instrumentations = [] @@ -218,10 +211,6 @@ function withVersions (plugin, modules, range, cb) { .forEach(v => { const versionPath = `${__dirname}/../../../../versions/${moduleName}@${v.test}/node_modules` - // afterEach contains currentTest data - // after doesn't contain test data nor know if any tests passed/failed - let moduleVersionDidFail = false - describe(`with ${moduleName} ${v.range} (${v.version})`, () => { let nodePath @@ -243,22 +232,7 @@ function withVersions (plugin, modules, range, cb) { cb(v.test, moduleName) - afterEach(function () { - if (this.currentTest.state === 'failed') { - moduleVersionDidFail = true - } - }) - after(() => { - console.log('MOCHA AFTER', moduleVersionDidFail, v.version) - if (moduleVersionDidFail) { - if (!packageVersionFailures[moduleName]) { - packageVersionFailures[moduleName] = new Set() - } - - packageVersionFailures[moduleName].add(v.version) - } - process.env.NODE_PATH = nodePath require('module').Module._initPaths() }) @@ -285,11 +259,6 @@ function withExports (moduleName, version, exportNames, versionRange, fn) { } exports.mochaHooks = { - // TODO: Figure out how to do this with tap too. - async afterAll () { - await slackReport(packageVersionFailures) - }, - afterEach () { agent.reset() runtimeMetrics.stop() diff --git a/packages/dd-trace/test/setup/slack-report.js b/packages/dd-trace/test/setup/slack-report.js deleted file mode 100644 index 36e69f8806d..00000000000 --- a/packages/dd-trace/test/setup/slack-report.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * This allows the test suite to post a Slack channel message when test failures related to packages occur. - * The intent is to run nightly and proactively discover incompatibilities as new packages are released. - * The Slack message contains data about the failing package as well as release dates for previous versions. - * This should help an on-call engineer quickly diagnose when an incompatibility was introduced. - */ - -/* eslint-disable no-console */ - -const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK -const SLACK_REPORT_ENABLE = process.env.SLACK_REPORT_ENABLE -const SLACK_MOREINFO = process.env.SLACK_MOREINFO - -const VERSION_EXTRACT = /^v?(\d+)\.(\d+)\.(\d+)$/ - -const FORMATTER = new Intl.RelativeTimeFormat('en-us', { numeric: 'auto' }) - -const TIME_THRESHOLDS = [ - { threshold: 60, unit: 'seconds' }, - { threshold: 60, unit: 'minutes' }, - { threshold: 24, unit: 'hours' }, - { threshold: 7, unit: 'days' }, - { threshold: 365 / 12 / 7, unit: 'weeks' }, - { threshold: 12, unit: 'months' }, - { threshold: Infinity, unit: 'years' } -] - -/** - * failures: { moduleName: Set(moduleVersion) } - */ -module.exports = async (failures) => { - if (!SLACK_REPORT_ENABLE) { - console.log('Slack Reporter is disabled') - return - } - - console.log('Slack Reporter is enabled') - - if (!SLACK_WEBHOOK) { - throw new Error('package reporting via slack webhook is enabled but misconfigured') - } - - const packageNames = Object.keys(failures) - - if (!packageNames.length) { - console.log('Slack Reporter has nothing to report') - return - } - - const descriptions = [] - - for (const packageName of packageNames) { - const versions = Array.from(failures[packageName]) - const description = await describe(packageName, versions) - descriptions.push(description) - } - - let message = descriptions.join('\n\n') - - if (SLACK_MOREINFO) { - // It's not easy to contextually link to individual job failures. - // @see https://github.com/community/community/discussions/8945 - // Instead we add a single link at the end to the overall run. - message += `\n<${SLACK_MOREINFO}|View the failing test(s) here>.` - } - - reportToSlack(message) -} - -async function describe (packageName, versions) { - const pairs = versions.map((v) => `\`${packageName}@${v}\``).join(' and ') - let output = `Nightly tests for ${pairs} are failing!\n` - - const suspects = getSuspectedVersions(versions) - const timestamps = await getVersionData(packageName) - - for (const version of suspects) { - output += `• version .\n` - } - - return output -} - -async function reportToSlack (message) { - console.log(message) - /* - await fetch(SLACK_WEBHOOK, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - text: message, - unfurl_links: false, - unfurl_media: false - }) - }) - */ -} - -async function getVersionData (packageName) { - const res = await fetch(`https://registry.npmjs.org/${packageName}`) - - const body = await res.json() - - const timestamps = body.time - - return timestamps -} - -// TODO: could just to 'semver' package and use .major(), etc, instead of regex -// returns the last three versions of a package that are the most likely to have caused a breaking change -// 1.2.3 -> "1.2.3", "1.2.0", "1.0.0" -// 3.0.0 -> "3.0.0" -function getSuspectedVersions (versions) { - const result = new Set() - - for (const version of versions) { - const [, major, minor, patch] = VERSION_EXTRACT.exec(version) - - result.add(`${major}.${minor}.${patch}`) - result.add(`${major}.${minor}.0`) - result.add(`${major}.0.0`) - } - - return Array.from(result) -} - -function formatTimeAgo (date) { - let duration = (date - new Date()) / 1000 - - for (const range of TIME_THRESHOLDS) { - if (Math.abs(duration) < range.threshold) { - return FORMATTER.format(Math.round(duration), range.unit) - } - duration /= range.threshold - } -} diff --git a/packages/dd-trace/test/telemetry/index.spec.js b/packages/dd-trace/test/telemetry/index.spec.js index e5480e429b1..c66818b41fa 100644 --- a/packages/dd-trace/test/telemetry/index.spec.js +++ b/packages/dd-trace/test/telemetry/index.spec.js @@ -96,25 +96,6 @@ describe('telemetry', () => { appsec: { enabled: true }, profiler: { version: tracerVersion, enabled: true } }) - expect(payload).to.have.property('configuration').that.deep.equal([ - { name: 'telemetry.enabled', value: true, origin: 'unknown' }, - { name: 'telemetry.heartbeatInterval', value: DEFAULT_HEARTBEAT_INTERVAL, origin: 'unknown' }, - { name: 'hostname', value: 'localhost', origin: 'unknown' }, - { name: 'port', value: traceAgent.address().port, origin: 'unknown' }, - { name: 'service', value: 'test service', origin: 'unknown' }, - { name: 'version', value: '1.2.3-beta4', origin: 'unknown' }, - { name: 'env', value: 'preprod', origin: 'unknown' }, - { name: 'tags.runtime-id', value: '1a2b3c', origin: 'unknown' }, - { name: 'circularObject.child.field', value: 'child_value', origin: 'unknown' }, - { name: 'circularObject.field', value: 'parent_value', origin: 'unknown' }, - { name: 'appsec.enabled', value: true, origin: 'unknown' }, - { name: 'profiling.enabled', value: true, origin: 'unknown' }, - { name: 'peerServiceMapping.service_1', value: 'remapped_service_1', origin: 'unknown' }, - { name: 'peerServiceMapping.service_2', value: 'remapped_service_2', origin: 'unknown' }, - { name: 'installSignature.id', value: '68e75c48-57ca-4a12-adfc-575c4b05fcbe', origin: 'unknown' }, - { name: 'installSignature.type', value: 'k8s_single_step', origin: 'unknown' }, - { name: 'installSignature.time', value: '1703188212', origin: 'unknown' } - ]) expect(payload).to.have.property('install_signature').that.deep.equal({ install_id: '68e75c48-57ca-4a12-adfc-575c4b05fcbe', install_type: 'k8s_single_step', diff --git a/register.js b/register.js new file mode 100644 index 00000000000..58adc77bd68 --- /dev/null +++ b/register.js @@ -0,0 +1,4 @@ +const { register } = require('node:module') +const { pathToFileURL } = require('node:url') + +register('./loader-hook.mjs', pathToFileURL(__filename)) diff --git a/yarn.lock b/yarn.lock index b75682b41d5..e3539b67a92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -412,10 +412,10 @@ resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" integrity "sha1-u1BFecHK6SPmV2pPXaQ9Jfl729k= sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" -"@datadog/native-appsec@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-7.0.0.tgz#a380174dd49aef2d9bb613a0ec8ead6dc7822095" - integrity sha512-bywstWFW2hWxzPuS0+mFMVHHL0geulx5yQFtsjfszaH2LTAgk2D+Rt40MKbAoZ8q3tRw2dy6aYQ7svO3ca8jpA== +"@datadog/native-appsec@7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-7.1.0.tgz#e8e6254236ac6fd7d4fb8b1156b34de64ec3e174" + integrity sha512-5FATunIxmvuSGDwPmbXfOi21wC7rjfbdLX4QiT5LR+iRLjRLT5iETqwdTsqy0WOQIHmxdWuddRvuakAg3921aA== dependencies: node-gyp-build "^3.9.0" @@ -442,10 +442,10 @@ node-addon-api "^6.1.0" node-gyp-build "^3.9.0" -"@datadog/pprof@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.0.0.tgz#0c0aaf06def6d2bc4b2d353ec7b264dadbfbefab" - integrity sha512-vhNan4SBuNWLpexunDJQ+hNbRAgWdk2qy5Iyh7Nn94uSSHXigAJMAvu4jwMKKQKFfchtobOkWT8GQUWW3tgpFg== +"@datadog/pprof@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.1.0.tgz#8ceec2569db9c62428ff073efc122ce038d9fb4d" + integrity sha512-2fDNHG9eMSiCMlnTodDdnEgZueQyXwQTR2IxhJx43x9CQz0zSjvzZH6W++N1CRwikmd6wPdqBv5KlzsWuEsOPg== dependencies: delay "^5.0.0" node-gyp-build "<4.0"