diff --git a/.gitlab/datasources/regions.yaml b/.gitlab/datasources/regions.yaml index 93816ce9..d41bade3 100644 --- a/.gitlab/datasources/regions.yaml +++ b/.gitlab/datasources/regions.yaml @@ -12,6 +12,7 @@ regions: - code: "ap-southeast-3" - code: "ap-southeast-4" - code: "ap-southeast-5" + - code: "ap-southeast-7" - code: "ap-northeast-1" - code: "ap-northeast-2" - code: "ap-northeast-3" @@ -28,4 +29,5 @@ regions: - code: "il-central-1" - code: "me-south-1" - code: "me-central-1" + - code: "mx-central-1" - code: "sa-east-1" diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 428a44c6..01f17ccc 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -15,7 +15,6 @@ ts-jest,dev,MIT,Copyright (c) 2016-2018 Kulshekhar Kabra tslint,dev,Apache-2.0,"Copyright 2013-2019 Palantir Technologies, Inc." typescript,dev,Apache-2.0,Copyright (c) Microsoft Corporation. dc-polyfill,import,MIT,"Copyright (c) 2023 Datadog, Inc." -hot-shots,import,MIT,Copyright 2011 Steve Ivy. All rights reserved. promise-retry,import,MIT,Copyright (c) 2014 IndigoUnited serialize-error,import,MIT,Copyright (c) Sindre Sorhus (https://sindresorhus.com) shimmer,import,BSD-2-Clause,"Copyright (c) 2013-2019, Forrest L Norvell" diff --git a/event_samples/application-load-balancer-multivalue-headers.json b/event_samples/application-load-balancer-multivalue-headers.json new file mode 100644 index 00000000..1502bbce --- /dev/null +++ b/event_samples/application-load-balancer-multivalue-headers.json @@ -0,0 +1,65 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:1234567890:targetgroup/nhulston-alb-test/dcabb42f66a496e0" + } + }, + "httpMethod": "GET", + "path": "/", + "multiValueQueryStringParameters": {}, + "multiValueHeaders": { + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate" + ], + "accept-language": [ + "*" + ], + "connection": [ + "keep-alive" + ], + "host": [ + "nhulston-test-0987654321.us-east-1.elb.amazonaws.com" + ], + "sec-fetch-mode": [ + "cors" + ], + "traceparent": [ + "00-68126c4300000000125a7f065cf9a530-1c6dcc8ab8a6e99d-01" + ], + "tracestate": [ + "dd=t.dm:-0;t.tid:68126c4300000000;s:1;p:1c6dcc8ab8a6e99d" + ], + "user-agent": [ + "node" + ], + "x-amzn-trace-id": [ + "Root=1-68126c45-01b175997ab51c4c47a2d643" + ], + "x-datadog-parent-id": [ + "1234567890" + ], + "x-datadog-sampling-priority": [ + "1" + ], + "x-datadog-tags": [ + "_dd.p.tid=68126c4300000000,_dd.p.dm=-0" + ], + "x-datadog-trace-id": [ + "0987654321" + ], + "x-forwarded-for": [ + "18.204.55.6" + ], + "x-forwarded-port": [ + "80" + ], + "x-forwarded-proto": [ + "http" + ] + }, + "body": "", + "isBase64Encoded": false +} diff --git a/package.json b/package.json index 1bf0563f..4f595611 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "datadog-lambda-js", - "version": "10.124.0", + "version": "11.125.0", "description": "Lambda client library that supports hybrid tracing in node js", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -29,7 +29,7 @@ "@types/node": "^20.12.10", "@types/promise-retry": "^1.1.3", "@types/shimmer": "^1.0.1", - "dd-trace": "^5.44.0", + "dd-trace": "^5.51.0", "jest": "^27.0.1", "mock-fs": "4.14.0", "nock": "13.5.4", @@ -41,7 +41,6 @@ "dependencies": { "@aws-crypto/sha256-js": "5.2.0", "dc-polyfill": "^0.1.3", - "hot-shots": "8.5.0", "promise-retry": "^2.0.1", "serialize-error": "^8.1.0", "shimmer": "1.2.1" diff --git a/src/index.spec.ts b/src/index.spec.ts index eba64684..d1b545f4 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -509,7 +509,6 @@ describe("datadog", () => { expect(mockedIncrementInvocations).toBeCalledTimes(1); expect(mockedIncrementInvocations).toBeCalledWith(expect.anything(), mockContext); - expect(logger.debug).toHaveBeenCalledTimes(8); expect(logger.debug).toHaveBeenLastCalledWith('{"status":"debug","message":"datadog:Unpatching HTTP libraries"}'); }); diff --git a/src/metrics/dogstatsd.spec.ts b/src/metrics/dogstatsd.spec.ts new file mode 100644 index 00000000..7c656e14 --- /dev/null +++ b/src/metrics/dogstatsd.spec.ts @@ -0,0 +1,87 @@ +import * as dgram from "node:dgram"; +import { LambdaDogStatsD } from "./dogstatsd"; + +jest.mock("node:dgram", () => ({ + createSocket: jest.fn(), +})); + +describe("LambdaDogStatsD", () => { + let mockSend: jest.Mock; + + beforeEach(() => { + // A send() that immediately calls its callback + mockSend = jest.fn((msg, port, host, cb) => cb()); + (dgram.createSocket as jest.Mock).mockReturnValue({ + send: mockSend, + getSendBufferSize: jest.fn().mockReturnValue(64 * 1024), + setSendBufferSize: jest.fn(), + bind: jest.fn(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("sends a distribution metric without tags or timestamp", async () => { + const client = new LambdaDogStatsD(); + client.distribution("metric", 1); + await client.flush(); + + expect(mockSend).toHaveBeenCalledWith(Buffer.from("metric:1|d", "utf8"), 8125, "127.0.0.1", expect.any(Function)); + }); + + it("sends with tags (sanitized) and timestamp", async () => { + const client = new LambdaDogStatsD(); + client.distribution("metric2", 2, 12345, ["tag1", "bad?tag"]); + await client.flush(); + + // "bad?tag" becomes "bad_tag" + expect(mockSend).toHaveBeenCalledWith( + Buffer.from("metric2:2|d|#tag1,bad_tag|T12345", "utf8"), + 8125, + "127.0.0.1", + expect.any(Function), + ); + }); + + it("rounds timestamp", async () => { + const client = new LambdaDogStatsD(); + client.distribution("metric2", 2, 12345.678); + await client.flush(); + + expect(mockSend).toHaveBeenCalledWith( + Buffer.from("metric2:2|d|T12345", "utf8"), + 8125, + "127.0.0.1", + expect.any(Function), + ); + }); + + it("flush() resolves immediately when there are no sends", async () => { + const client = new LambdaDogStatsD(); + await expect(client.flush()).resolves.toBeUndefined(); + }); + + it("flush() times out if a send never invokes its callback", async () => { + // replace socket.send with a never‐calling callback + (dgram.createSocket as jest.Mock).mockReturnValue({ + send: jest.fn(), // never calls callback + getSendBufferSize: jest.fn(), + setSendBufferSize: jest.fn(), + bind: jest.fn(), + }); + + const client = new LambdaDogStatsD(); + client.distribution("will", 9); + + jest.useFakeTimers(); + const p = client.flush(); + // advance past the 1000ms MAX_FLUSH_TIMEOUT + jest.advanceTimersByTime(1100); + + // expect the Promise returned by flush() to resolve successfully + await expect(p).resolves.toBeUndefined(); + jest.useRealTimers(); + }); +}); diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts new file mode 100644 index 00000000..4e3b6913 --- /dev/null +++ b/src/metrics/dogstatsd.ts @@ -0,0 +1,80 @@ +import * as dgram from "node:dgram"; +import { SocketType } from "node:dgram"; +import { logDebug } from "../utils"; + +export class LambdaDogStatsD { + private static readonly HOST = "127.0.0.1"; + private static readonly PORT = 8125; + private static readonly MIN_SEND_BUFFER_SIZE = 32 * 1024; + private static readonly ENCODING: BufferEncoding = "utf8"; + private static readonly SOCKET_TYPE: SocketType = "udp4"; + private static readonly TAG_RE = /[^\w\d_\-:\/\.]/gu; + private static readonly TAG_SUB = "_"; + // The maximum amount to wait while flushing pending sends, so we don't block forever. + private static readonly MAX_FLUSH_TIMEOUT = 1000; + + private readonly socket: dgram.Socket; + private readonly pendingSends = new Set>(); + + constructor() { + this.socket = dgram.createSocket(LambdaDogStatsD.SOCKET_TYPE); + } + + /** + * Send a distribution value, optionally setting tags and timestamp. + * Timestamp is seconds since epoch. + */ + public distribution(metric: string, value: number, timestamp?: number, tags?: string[]): void { + this.report(metric, "d", value, tags, timestamp); + } + + private normalizeTags(tags: string[]): string[] { + return tags.map((t) => t.replace(LambdaDogStatsD.TAG_RE, LambdaDogStatsD.TAG_SUB)); + } + + private report(metric: string, metricType: string, value: number | null, tags?: string[], timestamp?: number): void { + if (value == null) { + return; + } + + if (timestamp) { + timestamp = Math.floor(timestamp); + } + + const serializedTags = tags && tags.length ? `|#${this.normalizeTags(tags).join(",")}` : ""; + const timestampPart = timestamp != null ? `|T${timestamp}` : ""; + const payload = `${metric}:${value}|${metricType}${serializedTags}${timestampPart}`; + this.send(payload); + } + + private send(packet: string) { + const msg = Buffer.from(packet, LambdaDogStatsD.ENCODING); + const promise = new Promise((resolve) => { + this.socket.send(msg, LambdaDogStatsD.PORT, LambdaDogStatsD.HOST, (err) => { + if (err) { + logDebug(`Unable to send metric packet: ${err.message}`); + } + + resolve(); + }); + }); + + this.pendingSends.add(promise); + void promise.finally(() => this.pendingSends.delete(promise)); + } + + /** Block until all in-flight sends have settled */ + public async flush(): Promise { + const allSettled = Promise.allSettled(this.pendingSends); + const maxTimeout = new Promise<"timeout">((resolve) => { + setTimeout(() => resolve("timeout"), LambdaDogStatsD.MAX_FLUSH_TIMEOUT); + }); + + const winner = await Promise.race([allSettled, maxTimeout]); + if (winner === "timeout") { + logDebug("Timed out before sending all metric payloads"); + } + + this.pendingSends.clear(); + } +} diff --git a/src/metrics/kms-service.spec.ts b/src/metrics/kms-service.spec.ts index 6892884e..ffbe7ec7 100644 --- a/src/metrics/kms-service.spec.ts +++ b/src/metrics/kms-service.spec.ts @@ -122,22 +122,27 @@ describe("KMSService", () => { }); it("configures FIPS endpoint for GovCloud regions", async () => { - try { - process.env.AWS_REGION = "us-gov-west-1"; + jest.resetModules(); + + jest.mock("../utils/fips", () => ({ + AWS_REGION: "us-gov-west-1", + FIPS_MODE_ENABLED: true, + })); - const mockKmsConstructor = jest.fn(); - jest.mock("aws-sdk/clients/kms", () => mockKmsConstructor); - mockKmsConstructor.mockImplementation(() => ({ - decrypt: () => ({ - promise: () => Promise.resolve({ Plaintext: Buffer.from(EXPECTED_RESULT) }), - }), - })); + const mockKmsConstructor = jest.fn().mockImplementation(() => ({ + decrypt: () => ({ + promise: () => Promise.resolve({ Plaintext: Buffer.from(EXPECTED_RESULT) }), + }), + })); + jest.mock("aws-sdk/clients/kms", () => mockKmsConstructor); - // Create service and call decrypt + // tslint:disable-next-line:no-shadowed-variable + const { KMSService } = require("./kms-service"); + + try { const kmsService = new KMSService(); await kmsService.decrypt(ENCRYPTED_KEY); - // Verify FIPS endpoint was used expect(mockKmsConstructor).toHaveBeenCalledWith({ endpoint: "https://kms-fips.us-gov-west-1.amazonaws.com", }); diff --git a/src/metrics/kms-service.ts b/src/metrics/kms-service.ts index d10950f4..9094d388 100644 --- a/src/metrics/kms-service.ts +++ b/src/metrics/kms-service.ts @@ -2,6 +2,7 @@ // in the Lambda environment anyway), we use require to import the SDK. import { logDebug } from "../utils"; +import { AWS_REGION, FIPS_MODE_ENABLED } from "../utils/fips"; export class KMSService { private encryptionContext; @@ -14,16 +15,12 @@ export class KMSService { const buffer = Buffer.from(ciphertext, "base64"); let kms; - const region = process.env.AWS_REGION; - const isGovRegion = region !== undefined && region.startsWith("us-gov-"); - if (isGovRegion) { - logDebug("Govcloud region detected. Using FIPs endpoints for secrets management."); - } let kmsClientParams = {}; - if (isGovRegion) { + if (FIPS_MODE_ENABLED) { + logDebug("FIPS mode is enabled, Using FIPS endpoints for secrets management."); // Endpoints: https://docs.aws.amazon.com/general/latest/gr/kms.html kmsClientParams = { - endpoint: `https://kms-fips.${region}.amazonaws.com`, + endpoint: `https://kms-fips.${AWS_REGION}.amazonaws.com`, }; } diff --git a/src/metrics/listener.spec.ts b/src/metrics/listener.spec.ts index bcbc2deb..506b720c 100644 --- a/src/metrics/listener.spec.ts +++ b/src/metrics/listener.spec.ts @@ -5,9 +5,10 @@ import { LogLevel, setLogLevel } from "../utils"; import { EXTENSION_URL } from "./extension"; import { MetricsListener } from "./listener"; -import StatsDClient from "hot-shots"; +import { LambdaDogStatsD } from "./dogstatsd"; import { Context } from "aws-lambda"; -jest.mock("hot-shots"); + +jest.mock("./dogstatsd"); jest.mock("@aws-sdk/client-secrets-manager", () => { return { @@ -17,6 +18,9 @@ jest.mock("@aws-sdk/client-secrets-manager", () => { }; }); +const MOCK_TIME_SECONDS = 1487076708; +const MOCK_TIME_MS = 1487076708000; + const siteURL = "example.com"; class MockKMS { @@ -56,6 +60,7 @@ describe("MetricsListener", () => { expect(nock.isDone()).toBeTruthy(); }); + it("uses encrypted kms key if it's the only value available", async () => { nock("https://api.example.com").post("/api/v1/distribution_points?api_key=kms-api-key-decrypted").reply(200, {}); @@ -118,41 +123,11 @@ describe("MetricsListener", () => { await expect(listener.onCompleteInvocation()).resolves.toEqual(undefined); }); - it("configures FIPS endpoint for GovCloud regions", async () => { - try { - process.env.AWS_REGION = "us-gov-west-1"; - const secretsManagerModule = require("@aws-sdk/client-secrets-manager"); - const secretsManagerSpy = jest.spyOn(secretsManagerModule, "SecretsManager"); - - const kms = new MockKMS("kms-api-key-decrypted"); - const listener = new MetricsListener(kms as any, { - apiKey: "", - apiKeyKMS: "", - apiKeySecretARN: "arn:aws:secretsmanager:us-gov-west-1:1234567890:secret:key-name-123ABC", - enhancedMetrics: false, - logForwarding: false, - shouldRetryMetrics: false, - localTesting: false, - siteURL, - }); - - await listener.onStartInvocation({}); - await listener.onCompleteInvocation(); - - expect(secretsManagerSpy).toHaveBeenCalledWith({ - useFipsEndpoint: true, - region: "us-gov-west-1", - }); - - secretsManagerSpy.mockRestore(); - } finally { - process.env.AWS_REGION = "us-east-1"; - } - }); - it("uses correct secrets region", async () => { try { process.env.AWS_REGION = "us-east-1"; + + // tslint:disable-next-line:no-shadowed-variable const secretsManagerModule = require("@aws-sdk/client-secrets-manager"); const secretsManagerSpy = jest.spyOn(secretsManagerModule, "SecretsManager"); @@ -184,7 +159,7 @@ describe("MetricsListener", () => { it("logs metrics when logForwarding is enabled", async () => { const spy = jest.spyOn(process.stdout, "write"); - jest.spyOn(Date, "now").mockImplementation(() => 1487076708000); + jest.spyOn(Date.prototype, "getTime").mockReturnValue(MOCK_TIME_MS); const kms = new MockKMS("kms-api-key-decrypted"); const listener = new MetricsListener(kms as any, { apiKey: "api-key", @@ -202,22 +177,23 @@ describe("MetricsListener", () => { listener.sendDistributionMetric("my-metric", 10, false, "tag:a", "tag:b"); await listener.onCompleteInvocation(); - expect(spy).toHaveBeenCalledWith(`{"e":1487076708,"m":"my-metric","t":["tag:a","tag:b"],"v":10}\n`); + expect(spy).toHaveBeenCalledWith(`{"e":${MOCK_TIME_SECONDS},"m":"my-metric","t":["tag:a","tag:b"],"v":10}\n`); }); + it("always sends metrics to statsD when extension is enabled, ignoring logForwarding=true", async () => { const flushScope = nock(EXTENSION_URL).post("/lambda/flush", JSON.stringify({})).reply(200); mock({ "/opt/extensions/datadog-agent": Buffer.from([0]), }); const distributionMock = jest.fn(); - (StatsDClient as any).mockImplementation(() => { + (LambdaDogStatsD as any).mockImplementation(() => { return { distribution: distributionMock, close: (callback: any) => callback(undefined), }; }); - jest.spyOn(Date, "now").mockImplementation(() => 1487076708000); + jest.spyOn(Date.prototype, "getTime").mockReturnValue(MOCK_TIME_MS); const kms = new MockKMS("kms-api-key-decrypted"); const listener = new MetricsListener(kms as any, { @@ -236,10 +212,11 @@ describe("MetricsListener", () => { listener.sendDistributionMetric("my-metric", 10, false, "tag:a", "tag:b"); await listener.onCompleteInvocation(); expect(flushScope.isDone()).toBeTruthy(); - expect(distributionMock).toHaveBeenCalledWith("my-metric", 10, undefined, ["tag:a", "tag:b"]); + expect(distributionMock).toHaveBeenCalledWith("my-metric", 10, MOCK_TIME_SECONDS, ["tag:a", "tag:b"]); }); - it("only sends metrics with timestamps to the API when the extension is enabled", async () => { + it("sends metrics with timestamps to statsD (not API!) when the extension is enabled", async () => { + jest.spyOn(Date.prototype, "getTime").mockReturnValue(MOCK_TIME_MS); const flushScope = nock(EXTENSION_URL).post("/lambda/flush", JSON.stringify({})).reply(200); mock({ "/opt/extensions/datadog-agent": Buffer.from([0]), @@ -247,14 +224,14 @@ describe("MetricsListener", () => { const apiScope = nock("https://api.example.com").post("/api/v1/distribution_points?api_key=api-key").reply(200, {}); const distributionMock = jest.fn(); - (StatsDClient as any).mockImplementation(() => { + (LambdaDogStatsD as any).mockImplementation(() => { return { distribution: distributionMock, close: (callback: any) => callback(undefined), }; }); - const metricTimeOneMinuteAgo = new Date(Date.now() - 60000); + const metricTimeOneMinuteAgo = new Date(MOCK_TIME_MS - 60000); const kms = new MockKMS("kms-api-key-decrypted"); const listener = new MetricsListener(kms as any, { apiKey: "api-key", @@ -280,12 +257,15 @@ describe("MetricsListener", () => { "tag:a", "tag:b", ); - listener.sendDistributionMetric("my-metric-without-a-timestamp", 10, false, "tag:a", "tag:b"); + listener.sendDistributionMetric("my-metric-with-a-timestamp", 10, false, "tag:a", "tag:b"); await listener.onCompleteInvocation(); expect(flushScope.isDone()).toBeTruthy(); - expect(apiScope.isDone()).toBeTruthy(); - expect(distributionMock).toHaveBeenCalledWith("my-metric-without-a-timestamp", 10, undefined, ["tag:a", "tag:b"]); + expect(apiScope.isDone()).toBeFalsy(); + expect(distributionMock).toHaveBeenCalledWith("my-metric-with-a-timestamp", 10, MOCK_TIME_SECONDS, [ + "tag:a", + "tag:b", + ]); }); it("does not send historical metrics from over 4 hours ago to the API", async () => { @@ -316,7 +296,6 @@ describe("MetricsListener", () => { it("logs metrics when logForwarding is enabled with custom timestamp", async () => { const spy = jest.spyOn(process.stdout, "write"); - // jest.spyOn(Date, "now").mockImplementation(() => 1487076708000); const kms = new MockKMS("kms-api-key-decrypted"); const listener = new MetricsListener(kms as any, { apiKey: "api-key", @@ -328,7 +307,6 @@ describe("MetricsListener", () => { localTesting: false, siteURL, }); - // jest.useFakeTimers(); await listener.onStartInvocation({}); listener.sendDistributionMetricWithDate("my-metric", 10, new Date(1584983836 * 1000), false, "tag:a", "tag:b"); diff --git a/src/metrics/listener.ts b/src/metrics/listener.ts index 49cfa7c1..60cf0ded 100644 --- a/src/metrics/listener.ts +++ b/src/metrics/listener.ts @@ -1,4 +1,3 @@ -import { StatsD } from "hot-shots"; import { promisify } from "util"; import { logDebug, logError, logWarning } from "../utils"; import { flushExtension, isExtensionRunning } from "./extension"; @@ -7,7 +6,8 @@ import { writeMetricToStdout } from "./metric-log"; import { Distribution } from "./model"; import { Context } from "aws-lambda"; import { getEnhancedMetricTags } from "./enhanced-metrics"; -import { SecretsManagerClientConfig } from "@aws-sdk/client-secrets-manager"; +import { LambdaDogStatsD } from "./dogstatsd"; +import { FIPS_MODE_ENABLED } from "../utils/fips"; const METRICS_BATCH_SEND_INTERVAL = 10000; // 10 seconds const HISTORICAL_METRICS_THRESHOLD_HOURS = 4 * 60 * 60 * 1000; // 4 hours @@ -63,17 +63,24 @@ export interface MetricsConfig { export class MetricsListener { private currentProcessor?: Promise; - private apiKey: Promise; - private statsDClient?: StatsD; + private apiKey?: Promise; + private statsDClient: LambdaDogStatsD; private isExtensionRunning?: boolean = undefined; private globalTags?: string[] = []; constructor(private kmsClient: KMSService, private config: MetricsConfig) { - this.apiKey = this.getAPIKey(config); this.config = config; + this.statsDClient = new LambdaDogStatsD(); } public async onStartInvocation(_: any, context?: Context) { + // We get the API key in onStartInvocation rather than in the constructor because in busy functions, + // initialization may occur more than 5 minutes before the first invocation (due to proactive initialization), + // resulting in AWS errors: https://github.com/aws/aws-sdk-js-v3/issues/5192#issuecomment-2073243617 + if (!this.apiKey) { + this.apiKey = this.getAPIKey(this.config); + } + if (this.isExtensionRunning === undefined) { this.isExtensionRunning = await isExtensionRunning(); logDebug(`Extension present: ${this.isExtensionRunning}`); @@ -83,8 +90,6 @@ export class MetricsListener { logDebug(`Using StatsD client`); this.globalTags = this.getGlobalTags(context); - // About 200 chars per metric, so 8KB buffer size holds approx 40 metrics per request - this.statsDClient = new StatsD({ host: "127.0.0.1", closingFlushInterval: 1, maxBufferSize: 8192 }); return; } if (this.config.logForwarding) { @@ -109,19 +114,9 @@ export class MetricsListener { await processor.flush(); } - if (this.statsDClient !== undefined) { + if (this.isExtensionRunning) { logDebug(`Flushing statsD`); - - // Make sure all stats are flushed to extension - await new Promise((resolve, reject) => { - this.statsDClient?.close((error) => { - if (error !== undefined) { - reject(error); - } - resolve(); - }); - }); - this.statsDClient = undefined; + await this.statsDClient.flush(); } } catch (error) { // This can fail for a variety of reasons, from the API not being reachable, @@ -146,33 +141,37 @@ export class MetricsListener { forceAsync: boolean, ...tags: string[] ) { + // If extension is running, use dogstatsd (FIPS compliant) if (this.isExtensionRunning) { - const isMetricTimeValid = Date.parse(metricTime.toString()) > 0; - if (isMetricTimeValid) { - const dateCeiling = new Date(Date.now() - HISTORICAL_METRICS_THRESHOLD_HOURS); // 4 hours ago - if (dateCeiling > metricTime) { - logWarning(`Timestamp ${metricTime.toISOString()} is older than 4 hours, not submitting metric ${name}`); - return; - } - // Only create the processor to submit metrics to the API when a user provides a valid timestamp as - // Dogstatsd does not support timestamps for distributions. - if (this.currentProcessor === undefined) { - this.currentProcessor = this.createProcessor(this.config, this.apiKey); - } - // Add global tags to metrics sent to the API - if (this.globalTags !== undefined && this.globalTags.length > 0) { - tags = [...tags, ...this.globalTags]; - } - } else { - this.statsDClient?.distribution(name, value, undefined, tags); + const dateCeiling = new Date(Date.now() - HISTORICAL_METRICS_THRESHOLD_HOURS); // 4 hours ago + if (dateCeiling > metricTime) { + logWarning(`Timestamp ${metricTime.toISOString()} is older than 4 hours, not submitting metric ${name}`); return; } + + const secondsSinceEpoch = metricTime.getTime() / 1000; + this.statsDClient.distribution(name, value, secondsSinceEpoch, tags); + return; } + + // If no extension + logForwarding, write to stdout (FIPS compliant) if (this.config.logForwarding || forceAsync) { writeMetricToStdout(name, value, metricTime, tags); return; } + // Otherwise, send directly to DD API (not FIPs compliant!) + if (FIPS_MODE_ENABLED) { + logDebug( + "FIPS mode is enabled, so sending metrics via the Datadog API is not possible. (1) Install the extension, (2) enable log forwarding, or (3) disable FIPS mode.", + ); + return; + } + + // Add global tags to metrics sent to the API + if (this.globalTags !== undefined && this.globalTags.length > 0) { + tags = [...tags, ...this.globalTags]; + } const dist = new Distribution(name, [{ timestamp: metricTime, value }], ...tags); if (!this.apiKey) { @@ -191,9 +190,7 @@ export class MetricsListener { } public sendDistributionMetric(name: string, value: number, forceAsync: boolean, ...tags: string[]) { - // The Extension doesn't support distribution metrics with timestamps. Use sendDistributionMetricWithDate instead. - const metricTime = this.isExtensionRunning ? new Date(0) : new Date(Date.now()); - this.sendDistributionMetricWithDate(name, value, metricTime, forceAsync, ...tags); + this.sendDistributionMetricWithDate(name, value, new Date(), forceAsync, ...tags); } private async createProcessor(config: MetricsConfig, apiKey: Promise) { @@ -227,10 +224,8 @@ export class MetricsListener { try { const { SecretsManager } = await import("@aws-sdk/client-secrets-manager"); const secretRegion = config.apiKeySecretARN.split(":")[3]; - const lambdaRegion = process.env.AWS_REGION; - const isGovRegion = lambdaRegion !== undefined && lambdaRegion.startsWith("us-gov-"); const secretsManager = new SecretsManager({ - useFipsEndpoint: isGovRegion, + useFipsEndpoint: FIPS_MODE_ENABLED, region: secretRegion, }); const secret = await secretsManager.getSecretValue({ SecretId: config.apiKeySecretARN }); diff --git a/src/metrics/metric-log.spec.ts b/src/metrics/metric-log.spec.ts index e3c9107d..7b5f51b4 100644 --- a/src/metrics/metric-log.spec.ts +++ b/src/metrics/metric-log.spec.ts @@ -3,14 +3,14 @@ import { buildMetricLog } from "./metric-log"; describe("buildMetricLog", () => { it("handles empty tag list", () => { expect(buildMetricLog("my.test.metric", 1337, new Date(1487076708123), [])).toStrictEqual( - '{"e":1487076708.123,"m":"my.test.metric","t":[],"v":1337}\n', + '{"e":1487076708,"m":"my.test.metric","t":[],"v":1337}\n', ); }); it("writes timestamp in Unix seconds", () => { expect( buildMetricLog("my.test.metric", 1337, new Date(1487076708123), ["region:us", "account:dev", "team:serverless"]), ).toStrictEqual( - '{"e":1487076708.123,"m":"my.test.metric","t":["region:us","account:dev","team:serverless"],"v":1337}\n', + '{"e":1487076708,"m":"my.test.metric","t":["region:us","account:dev","team:serverless"],"v":1337}\n', ); }); }); diff --git a/src/metrics/metric-log.ts b/src/metrics/metric-log.ts index 1701d6dc..50288b26 100644 --- a/src/metrics/metric-log.ts +++ b/src/metrics/metric-log.ts @@ -2,7 +2,7 @@ export function buildMetricLog(name: string, value: number, metricTime: Date, tags: string[]) { return `${JSON.stringify({ // Date.now() returns Unix time in milliseconds, we convert to seconds for DD API submission - e: metricTime.getTime() / 1000, + e: Math.floor(metricTime.getTime() / 1000), m: name, t: tags, v: value, diff --git a/src/trace/context/extractor.ts b/src/trace/context/extractor.ts index b8a18253..fb690835 100644 --- a/src/trace/context/extractor.ts +++ b/src/trace/context/extractor.ts @@ -75,7 +75,8 @@ export class TraceContextExtractor { private getTraceEventExtractor(event: any): EventTraceExtractor | undefined { if (!event || typeof event !== "object") return; - if (event.headers !== null && typeof event.headers === "object") { + const headers = event.headers ?? event.multiValueHeaders; + if (headers !== null && typeof headers === "object") { return new HTTPEventTraceExtractor(this.tracerWrapper, this.config.decodeAuthorizerContext); } diff --git a/src/trace/context/extractors/http.spec.ts b/src/trace/context/extractors/http.spec.ts index f4e926e6..1cb7b3f4 100644 --- a/src/trace/context/extractors/http.spec.ts +++ b/src/trace/context/extractors/http.spec.ts @@ -1,5 +1,6 @@ import { TracerWrapper } from "../../tracer-wrapper"; import { HTTPEventSubType, HTTPEventTraceExtractor } from "./http"; +const albMultivalueHeadersEvent = require("../../../../event_samples/application-load-balancer-multivalue-headers.json"); let mockSpanContext: any = null; @@ -97,6 +98,66 @@ describe("HTTPEventTraceExtractor", () => { expect(traceContext?.source).toBe("event"); }); + it("extracts trace context from payload with multiValueHeaders", () => { + mockSpanContext = { + toTraceId: () => "123", + toSpanId: () => "456", + _sampling: { priority: "1" }, + }; + const tracerWrapper = new TracerWrapper(); + const payload = { + multiValueHeaders: { + "X-Datadog-Trace-Id": ["123", "789"], + "X-Datadog-Parent-Id": ["456"], + "X-Datadog-Sampling-Priority": ["1"], + }, + }; + const extractor = new HTTPEventTraceExtractor(tracerWrapper); + const traceContext = extractor.extract(payload); + + expect(traceContext).not.toBeNull(); + expect(spyTracerWrapper).toHaveBeenCalledWith({ + "x-datadog-trace-id": "123", + "x-datadog-parent-id": "456", + "x-datadog-sampling-priority": "1", + }); + + expect(traceContext?.toTraceId()).toBe("123"); + expect(traceContext?.toSpanId()).toBe("456"); + expect(traceContext?.sampleMode()).toBe("1"); + }); + + it("flattens a real ALB multiValueHeaders payload into a lowercase, single-value map", () => { + const tracerWrapper = new TracerWrapper(); + const extractor = new HTTPEventTraceExtractor(tracerWrapper); + + spyTracerWrapper.mockClear(); + extractor.extract(albMultivalueHeadersEvent); + expect(spyTracerWrapper).toHaveBeenCalled(); + + const captured = spyTracerWrapper.mock.calls[0][0] as Record; + + expect(captured).toEqual({ + accept: "*/*", + "accept-encoding": "gzip, deflate", + "accept-language": "*", + connection: "keep-alive", + host: "nhulston-test-0987654321.us-east-1.elb.amazonaws.com", + "sec-fetch-mode": "cors", + "user-agent": "node", + traceparent: "00-68126c4300000000125a7f065cf9a530-1c6dcc8ab8a6e99d-01", + tracestate: "dd=t.dm:-0;t.tid:68126c4300000000;s:1;p:1c6dcc8ab8a6e99d", + "x-amzn-trace-id": "Root=1-68126c45-01b175997ab51c4c47a2d643", + "x-datadog-tags": "_dd.p.tid=68126c4300000000,_dd.p.dm=-0", + "x-datadog-sampling-priority": "1", + "x-datadog-trace-id": "0987654321", + "x-datadog-parent-id": "1234567890", + "x-forwarded-for": "18.204.55.6", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + }); + }); + it("extracts trace context from payload with authorizer", () => { mockSpanContext = { toTraceId: () => "2389589954026090296", diff --git a/src/trace/context/extractors/http.ts b/src/trace/context/extractors/http.ts index 96192f93..2e57a21d 100644 --- a/src/trace/context/extractors/http.ts +++ b/src/trace/context/extractors/http.ts @@ -41,11 +41,17 @@ export class HTTPEventTraceExtractor implements EventTraceExtractor { } } - const headers = event.headers; + const headers = event.headers ?? event.multiValueHeaders; const lowerCaseHeaders: { [key: string]: string } = {}; - for (const key of Object.keys(headers)) { - lowerCaseHeaders[key.toLowerCase()] = headers[key]; + for (const [key, val] of Object.entries(headers)) { + if (Array.isArray(val)) { + // MultiValueHeaders: take the first value + lowerCaseHeaders[key.toLowerCase()] = val[0] ?? ""; + } else if (typeof val === "string") { + // Single‐value header + lowerCaseHeaders[key.toLowerCase()] = val; + } } const traceContext = this.tracerWrapper.extract(lowerCaseHeaders); diff --git a/src/utils/fips.spec.ts b/src/utils/fips.spec.ts new file mode 100644 index 00000000..f46ca21e --- /dev/null +++ b/src/utils/fips.spec.ts @@ -0,0 +1,44 @@ +describe("fips.ts", () => { + const ORIGINAL_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...ORIGINAL_ENV }; + }); + + afterAll(() => { + process.env = ORIGINAL_ENV; + }); + + it("enables FIPS mode in GovCloud by default", () => { + process.env.AWS_REGION = "us-gov-west-1"; + delete process.env.DD_LAMBDA_FIPS_MODE; + + const { FIPS_MODE_ENABLED } = require("./fips"); + expect(FIPS_MODE_ENABLED).toBe(true); + }); + + it("disables FIPS mode in standard region by default", () => { + process.env.AWS_REGION = "us-east-1"; + delete process.env.DD_LAMBDA_FIPS_MODE; + + const { FIPS_MODE_ENABLED } = require("./fips"); + expect(FIPS_MODE_ENABLED).toBe(false); + }); + + it("enables FIPS mode when env var is set to true in a standard region", () => { + process.env.AWS_REGION = "us-east-1"; + process.env.DD_LAMBDA_FIPS_MODE = "true"; + + const { FIPS_MODE_ENABLED } = require("./fips"); + expect(FIPS_MODE_ENABLED).toBe(true); + }); + + it("disables FIPS mode when DD_LAMBDA_FIPS_MODE=false in GovCloud region", () => { + process.env.AWS_REGION = "us-gov-east-1"; + process.env.DD_LAMBDA_FIPS_MODE = "false"; + + const { FIPS_MODE_ENABLED } = require("./fips"); + expect(FIPS_MODE_ENABLED).toBe(false); + }); +}); diff --git a/src/utils/fips.ts b/src/utils/fips.ts new file mode 100644 index 00000000..09d130b2 --- /dev/null +++ b/src/utils/fips.ts @@ -0,0 +1,13 @@ +import { logDebug } from "./log"; + +export const AWS_REGION = process.env.AWS_REGION ?? ""; +const isGovRegion = AWS_REGION.startsWith("us-gov-"); + +// Determine FIPS mode default (enabled in Gov regions) and override via env var +const defaultFips = isGovRegion ? "true" : "false"; +const rawFipsEnv = process.env.DD_LAMBDA_FIPS_MODE ?? defaultFips; +export const FIPS_MODE_ENABLED = rawFipsEnv.toLowerCase() === "true"; + +if (isGovRegion || FIPS_MODE_ENABLED) { + logDebug(`Node Lambda Layer FIPS mode is ${FIPS_MODE_ENABLED ? "enabled" : "not enabled"}.`); +} diff --git a/yarn.lock b/yarn.lock index d5dc60d4..7de56477 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1144,45 +1144,37 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@datadog/libdatadog@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.5.0.tgz#0ef2a2a76bb9505a0e7e5bc9be1415b467dbf368" - integrity sha512-YvLUVOhYVjJssm0f22/RnDQMc7ZZt/w1bA0nty1vvjyaDz5EWaHfWaaV4GYpCt5MRvnGjCBxIwwbRivmGseKeQ== +"@datadog/libdatadog@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.5.1.tgz#fe5c101c457998b74cb66f555f63197b34cad4ba" + integrity sha512-KsdOxTUmtjoygaZInSS5U0+KnqoxPKGpcBjGgOHR9NDKfXzmbpy5AmoaPL7JxmMxQzwknpxSi7qzBOSB3yMoJg== -"@datadog/native-appsec@8.5.1": - version "8.5.1" - resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.5.1.tgz#000fb06b91949d74298288ace5da51873922cc03" - integrity sha512-g6cjIafeObxVV+zJ2U1TDWVBio+MC8/4QR0EmgZ9afvhgtXRXyth3/DUOBSLUoMvCbduHwl6CV9sBf+tbSksVg== +"@datadog/native-appsec@8.5.2": + version "8.5.2" + resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.5.2.tgz#93a2c15c71c2a90e19e12506fbbdec9ccbc91541" + integrity sha512-lETBaVhBk+9o0pc+LDnXvp2ImDyT8K2deuqLf8A6q4/QjzCCXyR/yZO9R5+Kdoc93jZMRTWV9Pr4pBwHEdJSVA== dependencies: node-gyp-build "^3.9.0" -"@datadog/native-iast-rewriter@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.8.0.tgz#8a7eddf5e33266643afcdfb920ff5ccb30e1894a" - integrity sha512-DKmtvlmCld9RIJwDcPKWNkKYWYQyiuOrOtynmBppJiUv/yfCOuZtsQV4Zepj40H33sLiQyi5ct6dbWl53vxqkA== - dependencies: - lru-cache "^7.14.0" - node-gyp-build "^4.5.0" - -"@datadog/native-iast-taint-tracking@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.3.0.tgz#5a9c87e07376e7c5a4b4d4985f140a60388eee00" - integrity sha512-OzmjOncer199ATSYeCAwSACCRyQimo77LKadSHDUcxa/n9FYU+2U/bYQTYsK3vquSA2E47EbSVq9rytrlTdvnA== +"@datadog/native-iast-taint-tracking@3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.3.1.tgz#71d2c9bdb102b4482fea145d3f22ed5453628500" + integrity sha512-TgXpoX/CDgPfYAKu9qLmEyb9UXvRVC00D71islcSb70MCFmxQwkgXGl/gAk6YA6/NmZ4j8+cgY1lSNqStGvOMg== dependencies: node-gyp-build "^3.9.0" -"@datadog/native-metrics@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.1.0.tgz#c2378841accd9fdd6866d0e49bdf6e3d76e79f22" - integrity sha512-yOBi4x0OQRaGNPZ2bx9TGvDIgEdQ8fkudLTFAe7gEM1nAlvFmbE5YfpH8WenEtTSEBwojSau06m2q7axtEEmCg== +"@datadog/native-metrics@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.1.1.tgz#4e5c9775751af13e353e64e573ab724104538cee" + integrity sha512-MU1gHrolwryrU4X9g+fylA1KPH3S46oqJPEtVyrO+3Kh29z80fegmtyrU22bNt8LigPUK/EdPCnSbMe88QbnxQ== dependencies: node-addon-api "^6.1.0" node-gyp-build "^3.9.0" -"@datadog/pprof@5.6.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.6.0.tgz#b6f5c566512ba5e55c6dbf46e9f0f020cfd5c6b5" - integrity sha512-x7yN0s4wMnRqv3PWQ6eXKH5XE5qvCOwWbOsXqpT2Irbsc7Wcl5w5JrJUcbPCdSJGihpIh6kAeIrS6w/ZCcHy2Q== +"@datadog/pprof@5.7.1": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.7.1.tgz#3ed62372af7331c37de401319bde9e3d4dc5a8c0" + integrity sha512-D5XTxsaPG36x41vZZn8hsAeC7QQDx0rv1a1Uhxo5xCXUB/9rc19+I7iCnjgJS5aH0ShXdPVOWRClo16hOSKKSw== dependencies: delay "^5.0.0" node-gyp-build "<4.0" @@ -1195,6 +1187,16 @@ resolved "https://registry.yarnpkg.com/@datadog/sketches-js/-/sketches-js-2.1.1.tgz#9ec2251b3c932b4f43e1d164461fa6cb6f28b7d0" integrity sha512-d5RjycE+MObE/hU+8OM5Zp4VjTwiPLRa8299fj7muOmR16fb942z8byoMbCErnGh0lBevvgkGrLclQDvINbIyg== +"@datadog/wasm-js-rewriter@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@datadog/wasm-js-rewriter/-/wasm-js-rewriter-4.0.1.tgz#883535e97f6b88b15427f93b5dc5d2d3a01c02b6" + integrity sha512-JRa05Je6gw+9+3yZnm/BroQZrEfNwRYCxms56WCCHzOBnoPihQLB0fWy5coVJS29kneCUueUvBvxGp6NVXgdqw== + dependencies: + js-yaml "^4.1.0" + lru-cache "^7.14.0" + module-details-from-path "^1.0.3" + node-gyp-build "^4.5.0" + "@isaacs/ttlcache@^1.4.1": version "1.4.1" resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" @@ -2466,6 +2468,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2568,13 +2575,6 @@ base64-js@^1.0.2: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - bowser@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" @@ -2816,28 +2816,33 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -dc-polyfill@0.1.6, dc-polyfill@^0.1.3: +dc-polyfill@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.8.tgz#2d91dd4dd0f2e3575ce038d013f346161f5a413a" + integrity sha512-F9+06papa9GOFUMjxGiqM1bS98pOkinZpBF3Sygb46owrXaHdR2uLkftE6nygrqNcAurdwKjLAtX+0GJkSwIFQ== + +dc-polyfill@^0.1.3: version "0.1.6" resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.6.tgz#c2940fa68ffb24a7bf127cc6cfdd15b39f0e7f02" integrity sha512-UV33cugmCC49a5uWAApM+6Ev9ZdvIUMTrtCO9fj96TPGOQiea54oeO3tiEVdVeo3J9N2UdJEmbS4zOkkEA35uQ== -dd-trace@^5.44.0: - version "5.44.0" - resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-5.44.0.tgz#58e32b2e2ccbae08019d9771e5a7d9f20f676c4a" - integrity sha512-Ucl767ngM/pA1NPatmq9zG2B2Rb7pb2kMXiiVe7K17ZWF+M3WvVIRtf5FTl7wldd2FbN6E7AKYdBmwUYFtHmIQ== - dependencies: - "@datadog/libdatadog" "^0.5.0" - "@datadog/native-appsec" "8.5.1" - "@datadog/native-iast-rewriter" "2.8.0" - "@datadog/native-iast-taint-tracking" "3.3.0" - "@datadog/native-metrics" "^3.1.0" - "@datadog/pprof" "5.6.0" +dd-trace@^5.51.0: + version "5.51.0" + resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-5.51.0.tgz#063c92fdc96065ac164942f2f76c7e906863487d" + integrity sha512-/LH1RAoTau2KileM5mx58oGPBhdjejCYMqSMbbe0Rcqkipn/9RaxNMePGcfrnEa7Rwmar8PjPfDM5m7nn2d+Aw== + dependencies: + "@datadog/libdatadog" "^0.5.1" + "@datadog/native-appsec" "8.5.2" + "@datadog/native-iast-taint-tracking" "3.3.1" + "@datadog/native-metrics" "^3.1.1" + "@datadog/pprof" "5.7.1" "@datadog/sketches-js" "^2.1.0" + "@datadog/wasm-js-rewriter" "4.0.1" "@isaacs/ttlcache" "^1.4.1" "@opentelemetry/api" ">=1.0.0 <1.9.0" "@opentelemetry/core" "^1.14.0" crypto-randomuuid "^1.0.0" - dc-polyfill "0.1.6" + dc-polyfill "0.1.8" ignore "^5.2.4" import-in-the-middle "1.13.1" istanbul-lib-coverage "3.2.0" @@ -2847,6 +2852,7 @@ dd-trace@^5.44.0: lodash.sortby "^4.7.0" lru-cache "^7.14.0" module-details-from-path "^1.0.3" + mutexify "^1.4.0" opentracing ">=0.12.1" path-to-regexp "^0.1.12" pprof-format "^2.1.0" @@ -3066,11 +3072,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -3218,13 +3219,6 @@ hasown@^2.0.0, hasown@^2.0.2: dependencies: function-bind "^1.1.2" -hot-shots@8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/hot-shots/-/hot-shots-8.5.0.tgz#860246a3694dfb74cfe6045501eb59fb57e16eb9" - integrity sha512-GNXtNSxa9qibcPhi3gndyN5g14iBJS+/DDlu7hjSPfXYJy9/fcO13DgSyfPUVWrD/aIyPY36z7MksHvDe05zYg== - optionalDependencies: - unix-dgram "2.0.x" - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -3888,6 +3882,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsdom@^16.6.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" @@ -4088,10 +4089,12 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nan@^2.16.0: - version "2.22.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3" - integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== +mutexify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/mutexify/-/mutexify-1.4.0.tgz#b7f4ac0273c81824b840887c6a6e0bfab14bbe94" + integrity sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg== + dependencies: + queue-tick "^1.0.0" natural-compare@^1.4.0: version "1.4.0" @@ -4354,6 +4357,11 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== +queue-tick@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" @@ -4778,14 +4786,6 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== -unix-dgram@2.0.x: - version "2.0.6" - resolved "https://registry.yarnpkg.com/unix-dgram/-/unix-dgram-2.0.6.tgz#6d567b0eb6d7a9504e561532b598a46e34c5968b" - integrity sha512-AURroAsb73BZ6CdAyMrTk/hYKNj3DuYYEuOaB8bYMOHGKupRNScw90Q5C71tWJc3uE7dIeXRyuwN0xLLq3vDTg== - dependencies: - bindings "^1.5.0" - nan "^2.16.0" - update-browserslist-db@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"