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/package.json b/package.json index 1bf0563f..1eec5b34 100644 --- a/package.json +++ b/package.json @@ -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/listener.spec.ts b/src/metrics/listener.spec.ts index bcbc2deb..8cb9b1b8 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, {}); @@ -184,7 +189,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 +207,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 +242,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 +254,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 +287,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 +326,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 +337,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..bc349f0b 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,7 @@ 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"; const METRICS_BATCH_SEND_INTERVAL = 10000; // 10 seconds const HISTORICAL_METRICS_THRESHOLD_HOURS = 4 * 60 * 60 * 1000; // 4 hours @@ -64,13 +63,14 @@ export interface MetricsConfig { export class MetricsListener { private currentProcessor?: Promise; private apiKey: Promise; - private statsDClient?: StatsD; + 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) { @@ -83,8 +83,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 +107,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 +134,30 @@ 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!) + // 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 +176,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) { 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/yarn.lock b/yarn.lock index d5dc60d4..db10bc96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2568,13 +2568,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" @@ -3066,11 +3059,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 +3206,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" @@ -4088,11 +4069,6 @@ 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== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -4778,14 +4754,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"