diff --git a/packages/core/package.json b/packages/core/package.json index cf4762b11d60..a329badfd523 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -79,6 +79,7 @@ }, "license": "Apache-2.0", "dependencies": { + "@aws-sdk/types": "*", "@smithy/core": "^2.4.7", "@smithy/node-config-provider": "^3.1.8", "@smithy/property-provider": "^3.1.7", diff --git a/packages/core/src/submodules/client/index.ts b/packages/core/src/submodules/client/index.ts index ed9af92686a2..2c463d5f978f 100644 --- a/packages/core/src/submodules/client/index.ts +++ b/packages/core/src/submodules/client/index.ts @@ -1 +1,2 @@ export * from "./emitWarningIfUnsupportedVersion"; +export * from "./setFeature"; diff --git a/packages/core/src/submodules/client/setFeature.spec.ts b/packages/core/src/submodules/client/setFeature.spec.ts new file mode 100644 index 000000000000..729867ce7d97 --- /dev/null +++ b/packages/core/src/submodules/client/setFeature.spec.ts @@ -0,0 +1,10 @@ +import { AwsHandlerExecutionContext } from "@aws-sdk/types"; + +import { setFeature } from "./setFeature"; + +describe(setFeature.name, () => { + it("creates the context object path if needed", () => { + const context: AwsHandlerExecutionContext = {}; + setFeature(context, "ACCOUNT_ID_ENDPOINT", "O"); + }); +}); diff --git a/packages/core/src/submodules/client/setFeature.ts b/packages/core/src/submodules/client/setFeature.ts new file mode 100644 index 000000000000..b4e3a1cf7ef5 --- /dev/null +++ b/packages/core/src/submodules/client/setFeature.ts @@ -0,0 +1,27 @@ +import type { AwsHandlerExecutionContext, AwsSdkFeatures } from "@aws-sdk/types"; + +/** + * @internal + * Sets the feature for the request context to be read in the user agent + * middleware. + * + * @param context - handler execution context. + * @param feature - readable name of feature. + * @param value - encoding value of feature. This is required because the + * specification asks the SDK not to include a runtime lookup of all + * the feature identifiers. + */ +export function setFeature( + context: AwsHandlerExecutionContext, + feature: F, + value: AwsSdkFeatures[F] +) { + if (!context.__aws_sdk_context) { + context.__aws_sdk_context = { + features: {}, + }; + } else if (!context.__aws_sdk_context.features) { + context.__aws_sdk_context.features = {}; + } + context.__aws_sdk_context.features![feature] = value; +} diff --git a/packages/middleware-user-agent/src/encode-metrics.spec.ts b/packages/middleware-user-agent/src/encode-metrics.spec.ts new file mode 100644 index 000000000000..0b1ceb7e9de2 --- /dev/null +++ b/packages/middleware-user-agent/src/encode-metrics.spec.ts @@ -0,0 +1,26 @@ +import { encodeMetrics } from "./encode-metrics"; + +describe(encodeMetrics.name, () => { + it("encodes empty metrics", () => { + expect(encodeMetrics({})).toEqual(""); + }); + + it("encodes metrics", () => { + expect( + encodeMetrics({ + A: "A", + z: "z", + } as any) + ).toEqual("A,z"); + }); + + it("drops values that would exceed 1024 bytes", () => { + expect( + encodeMetrics({ + A: "A".repeat(512), + B: "B".repeat(511), + z: "z", + } as any) + ).toEqual("A".repeat(512) + "," + "B".repeat(511)); + }); +}); diff --git a/packages/middleware-user-agent/src/encode-metrics.ts b/packages/middleware-user-agent/src/encode-metrics.ts new file mode 100644 index 000000000000..2136a3f7372d --- /dev/null +++ b/packages/middleware-user-agent/src/encode-metrics.ts @@ -0,0 +1,28 @@ +import type { AwsSdkFeatures } from "@aws-sdk/types"; + +const BYTE_LIMIT = 1024; + +/** + * @internal + */ +export function encodeMetrics(metrics: AwsSdkFeatures): string { + let buffer = ""; + + // currently all possible values are 1 byte, + // so string length is used. + + for (const key in metrics) { + const val = metrics[key as keyof typeof metrics]!; + if (buffer.length + val!.length + 1 <= BYTE_LIMIT) { + if (buffer.length) { + buffer += "," + val; + } else { + buffer += val; + } + continue; + } + break; + } + + return buffer; +} diff --git a/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts b/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts index f95b8f78cf19..aa2c67fef18c 100644 --- a/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts +++ b/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts @@ -14,7 +14,7 @@ describe("middleware-user-agent", () => { requireRequestsFrom(client).toMatch({ headers: { "x-amz-user-agent": /aws-sdk-js\/[\d\.]+/, - "user-agent": /aws-sdk-js\/[\d\.]+ (.*?)lang\/js md\/nodejs\#[\d\.]+ (.*?)api\/(.+)\#[\d\.]+/, + "user-agent": /aws-sdk-js\/[\d\.]+ (.*?)lang\/js md\/nodejs\#[\d\.]+ (.*?)api\/(.+)\#[\d\.]+ (.*?)m\//, }, }); await client.getUserDetails({ diff --git a/packages/middleware-user-agent/src/user-agent-middleware.spec.ts b/packages/middleware-user-agent/src/user-agent-middleware.spec.ts index b971fb2588f1..dc3bd44d2e52 100644 --- a/packages/middleware-user-agent/src/user-agent-middleware.spec.ts +++ b/packages/middleware-user-agent/src/user-agent-middleware.spec.ts @@ -50,6 +50,37 @@ describe("userAgentMiddleware", () => { ); }); + describe("metrics", () => { + it("should collect metrics from the context", async () => { + const middleware = userAgentMiddleware({ + defaultUserAgentProvider: async () => [ + ["default_agent", "1.0.0"], + ["aws-sdk-js", "1.0.0"], + ], + runtime: "node", + }); + + const handler = middleware(mockNextHandler, { + __aws_sdk_context: { + features: { + "0": "0", + "9": "9", + A: "A", + B: "B", + y: "y", + z: "z", + "+": "+", + "/": "/", + }, + }, + }); + await handler({ input: {}, request: new HttpRequest({ headers: {} }) }); + expect(mockNextHandler.mock.calls[0][0].request.headers[USER_AGENT]).toEqual( + expect.stringContaining(`m/0,9,A,B,y,z,+,/`) + ); + }); + }); + describe("should sanitize the SDK user agent string", () => { const cases: { ua: UserAgentPair; expected: string }[] = [ { ua: ["/name", "1.0.0"], expected: "name/1.0.0" }, diff --git a/packages/middleware-user-agent/src/user-agent-middleware.ts b/packages/middleware-user-agent/src/user-agent-middleware.ts index 456f967b7d8a..f95e191aa299 100644 --- a/packages/middleware-user-agent/src/user-agent-middleware.ts +++ b/packages/middleware-user-agent/src/user-agent-middleware.ts @@ -1,3 +1,4 @@ +import type { AwsHandlerExecutionContext } from "@aws-sdk/types"; import { getUserAgentPrefix } from "@aws-sdk/util-endpoints"; import { HttpRequest } from "@smithy/protocol-http"; import { @@ -22,6 +23,7 @@ import { USER_AGENT, X_AMZ_USER_AGENT, } from "./constants"; +import { encodeMetrics } from "./encode-metrics"; /** * Build user agent header sections from: @@ -39,14 +41,18 @@ export const userAgentMiddleware = (options: UserAgentResolvedConfig) => ( next: BuildHandler, - context: HandlerExecutionContext + context: HandlerExecutionContext | AwsHandlerExecutionContext ): BuildHandler => async (args: BuildHandlerArguments): Promise> => { const { request } = args; - if (!HttpRequest.isInstance(request)) return next(args); + if (!HttpRequest.isInstance(request)) { + return next(args); + } const { headers } = request; const userAgent = context?.userAgent?.map(escapeUserAgent) || []; const defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent); + const awsContext = context as AwsHandlerExecutionContext; + defaultUserAgent.push(`m/${encodeMetrics(awsContext.__aws_sdk_context?.features ?? {})}`); const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || []; const prefix = getUserAgentPrefix(); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ab64cd947b8c..ce32277d0364 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -12,6 +12,7 @@ export * from "./encode"; export * from "./endpoint"; export * from "./eventStream"; export * from "./extensions"; +export * from "./feature-ids"; export * from "./http"; export * from "./identity"; export * from "./logger"; diff --git a/packages/types/src/middleware.ts b/packages/types/src/middleware.ts index 3ae51bd9a83d..9cdc79fe2e60 100644 --- a/packages/types/src/middleware.ts +++ b/packages/types/src/middleware.ts @@ -1,3 +1,7 @@ +import { HandlerExecutionContext } from "@smithy/types"; + +import { AwsSdkFeatures } from "./feature-ids"; + export { AbsoluteLocation, BuildHandler, @@ -38,3 +42,14 @@ export { Step, Terminalware, } from "@smithy/types"; + +/** + * @internal + * Contains reserved keys for AWS SDK internal usage of the + * handler execution context object. + */ +export interface AwsHandlerExecutionContext extends HandlerExecutionContext { + __aws_sdk_context?: { + features?: AwsSdkFeatures; + }; +} diff --git a/packages/util-user-agent-browser/src/index.native.spec.ts b/packages/util-user-agent-browser/src/index.native.spec.ts index 56313e41305f..b7d60589d116 100644 --- a/packages/util-user-agent-browser/src/index.native.spec.ts +++ b/packages/util-user-agent-browser/src/index.native.spec.ts @@ -6,7 +6,7 @@ it("should response basic browser default user agent", async () => { jest.spyOn(window.navigator, "userAgent", "get").mockReturnValue(undefined); const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })(); expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]); - expect(userAgent[1]).toEqual(["ua", "2.0"]); + expect(userAgent[1]).toEqual(["ua", "2.1"]); expect(userAgent[2]).toEqual(["os/other"]); expect(userAgent[3]).toEqual(["lang/js"]); expect(userAgent[4]).toEqual(["md/rn"]); diff --git a/packages/util-user-agent-browser/src/index.native.ts b/packages/util-user-agent-browser/src/index.native.ts index 2d54ad3de29e..aca09d7ce7f3 100644 --- a/packages/util-user-agent-browser/src/index.native.ts +++ b/packages/util-user-agent-browser/src/index.native.ts @@ -15,7 +15,7 @@ export const defaultUserAgent = // sdk-metadata ["aws-sdk-js", clientVersion], // ua-metadata - ["ua", "2.0"], + ["ua", "2.1"], // os-metadata ["os/other"], // language-metadata diff --git a/packages/util-user-agent-browser/src/index.spec.ts b/packages/util-user-agent-browser/src/index.spec.ts index c290ff932168..8d316d4e2d0a 100644 --- a/packages/util-user-agent-browser/src/index.spec.ts +++ b/packages/util-user-agent-browser/src/index.spec.ts @@ -7,7 +7,7 @@ it("should populate metrics", async () => { jest.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua); const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })(); expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]); - expect(userAgent[1]).toEqual(["ua", "2.0"]); + expect(userAgent[1]).toEqual(["ua", "2.1"]); expect(userAgent[2]).toEqual(["os/macOS", "10.15.7"]); expect(userAgent[3]).toEqual(["lang/js"]); expect(userAgent[4]).toEqual(["md/browser", "Chrome_86.0.4240.111"]); diff --git a/packages/util-user-agent-browser/src/index.ts b/packages/util-user-agent-browser/src/index.ts index 6fddd18de874..bd0d33d5b3c9 100644 --- a/packages/util-user-agent-browser/src/index.ts +++ b/packages/util-user-agent-browser/src/index.ts @@ -20,7 +20,7 @@ export const defaultUserAgent = // sdk-metadata ["aws-sdk-js", clientVersion], // ua-metadata - ["ua", "2.0"], + ["ua", "2.1"], // os-metadata [`os/${parsedUA?.os?.name || "other"}`, parsedUA?.os?.version], // language-metadata diff --git a/packages/util-user-agent-node/src/index.spec.ts b/packages/util-user-agent-node/src/index.spec.ts index 07ff0a30a86e..3ffe30315a66 100644 --- a/packages/util-user-agent-node/src/index.spec.ts +++ b/packages/util-user-agent-node/src/index.spec.ts @@ -43,7 +43,7 @@ describe("defaultUserAgent", () => { const basicUserAgent: UserAgent = [ ["aws-sdk-js", "0.1.0"], - ["ua", "2.0"], + ["ua", "2.1"], ["api/s3", "0.1.0"], ["os/darwin", "19.6.0"], ["lang/js"], diff --git a/packages/util-user-agent-node/src/index.ts b/packages/util-user-agent-node/src/index.ts index f8c09d51104f..d3cec4de2cfb 100644 --- a/packages/util-user-agent-node/src/index.ts +++ b/packages/util-user-agent-node/src/index.ts @@ -31,7 +31,7 @@ export const defaultUserAgent = ({ serviceId, clientVersion }: DefaultUserAgentO // sdk-metadata ["aws-sdk-js", clientVersion], // ua-metadata - ["ua", "2.0"], + ["ua", "2.1"], // os-metadata [`os/${platform()}`, release()], // language-metadata