Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2afbfb5
feat(codegen): add codegen for userAgentAppId
siddsriv Sep 26, 2024
c973314
chore(codegen): comment update
siddsriv Sep 26, 2024
70f7722
fix(codegen): formatting fixes
siddsriv Sep 26, 2024
11fb1b9
fix(codegen): correct appId type
siddsriv Sep 26, 2024
86b6c2d
chore(codegen): comments
siddsriv Sep 26, 2024
df70919
chore(codegen): comment update
siddsriv Sep 26, 2024
d731ec6
chore(codegen): merge appId codegen into existing UA codegen
siddsriv Sep 27, 2024
b9e9e19
chore(codegen): update typescript integration for codegen change in p…
siddsriv Sep 27, 2024
22dd421
chore(codegen): keep codegen style consistent for runtimeConfigs
siddsriv Sep 27, 2024
49e3086
chore(codegen): style/comment update
siddsriv Sep 27, 2024
ad89eac
feat(middleware-user-agent): appId config resolvers and accompanying …
siddsriv Sep 30, 2024
b47f801
fix(middleware-user-agent): add smithy core in deps
siddsriv Sep 30, 2024
a067a37
test(middleware-user-agent): add case for appId
siddsriv Sep 30, 2024
1c079c5
test(middleware-user-agent): case for long appId
siddsriv Sep 30, 2024
9e0ed22
feat(util-user-agent-browser): add appId collection
siddsriv Sep 30, 2024
919f209
test(util-user-agent-node): add appId case
siddsriv Sep 30, 2024
8707e8d
test(util-user-agent-node): test update for mock env
siddsriv Oct 1, 2024
ce01280
chore(middleware-user-agent): formatting fixes
siddsriv Oct 1, 2024
bceeb93
docs(supplemental-docs): add userAgentAppId in CLIENTS
siddsriv Oct 1, 2024
79cf144
docs(supplemental-docs): correct example
siddsriv Oct 1, 2024
efd2bae
chore(clients): codege
siddsriv Oct 1, 2024
1b7cb36
test(util-user-agent-node): unit test update to mock before import
siddsriv Oct 1, 2024
1b8c57a
Merge branch 'main' into feat/UAAppId
siddsriv Oct 1, 2024
51edd2d
chore(util-user-agent-node): remove explicit return type
siddsriv Oct 2, 2024
6b97cc8
chore(private): add userAgentAppId explicitly for interface test fix
siddsriv Oct 2, 2024
7d656b0
chore(middleware-user-agent): deps update
siddsriv Oct 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Add client plubins and configs to support injecting user agent.
* Add client plugins and configs to support injecting user agent.
*/
@SmithyInternalApi
public class AddUserAgentDependency implements TypeScriptIntegration {
Expand All @@ -57,6 +57,9 @@ public void addConfigInterfaceFields(
writer.writeDocs("The provider populating default tracking information to be sent with `user-agent`, "
+ "`x-amz-user-agent` header\n@internal");
writer.write("defaultUserAgentProvider?: Provider<__UserAgent>;\n");
writer.writeDocs("The application ID used to identify the application.");
writer.write("userAgentAppId?: string | "
+ "__Provider<string>;\n");
}

@Override
Expand All @@ -76,6 +79,15 @@ public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
writer.addIgnoredDefaultImport("packageInfo", "./package.json",
"package.json will be imported from dist folders");
writeDefaultUserAgentProvider(writer, settings, model);
},
"userAgentAppId", writer -> {
writer.addDependency(TypeScriptDependency.NODE_CONFIG_PROVIDER);
writer.addImport("loadConfig", "loadNodeConfig",
TypeScriptDependency.NODE_CONFIG_PROVIDER);
writer.addDependency(AwsDependency.AWS_SDK_UTIL_USER_AGENT_NODE);
writer.addImport("NODE_APP_ID_CONFIG_OPTIONS", "NODE_APP_ID_CONFIG_OPTIONS",
AwsDependency.AWS_SDK_UTIL_USER_AGENT_NODE);
writer.write("loadNodeConfig(NODE_APP_ID_CONFIG_OPTIONS)");
}
);
case BROWSER:
Expand Down
1 change: 1 addition & 0 deletions packages/middleware-user-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"dependencies": {
"@aws-sdk/types": "*",
"@aws-sdk/util-endpoints": "*",
"@smithy/core": "*",
"@smithy/protocol-http": "^4.1.3",
"@smithy/types": "^3.4.2",
"tslib": "^2.6.2"
Expand Down
43 changes: 41 additions & 2 deletions packages/middleware-user-agent/src/configurations.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Provider, UserAgent } from "@smithy/types";
import { Logger, Provider, UserAgent } from "@smithy/types";
import { normalizeProvider } from "@smithy/core";

/**
* @internal
*/
export const DEFAULT_UA_APP_ID = undefined;

/**
* @public
*/
Expand All @@ -7,11 +14,18 @@ export interface UserAgentInputConfig {
* The custom user agent header that would be appended to default one
*/
customUserAgent?: string | UserAgent;
/**
* The application ID used to identify the SDK client.
*/
userAgentAppId?: string | undefined | Provider<string | undefined>;
}

interface PreviouslyResolved {
defaultUserAgentProvider: Provider<UserAgent>;
runtime: string;
logger?: Logger;
}

export interface UserAgentResolvedConfig {
/**
* The provider populating default tracking information to be sent with `user-agent`, `x-amz-user-agent` header.
Expand All @@ -26,12 +40,37 @@ export interface UserAgentResolvedConfig {
* The runtime environment
*/
runtime: string;
/**
* Resolved value for input config {config.userAgentAppId}
*/
userAgentAppId: Provider<string | undefined>;
}

function isValidUserAgentAppId(appId: unknown): boolean {
if (appId === undefined) {
return true;
}
return typeof appId === "string" && appId.length <= 50;
}

export function resolveUserAgentConfig<T>(
input: T & PreviouslyResolved & UserAgentInputConfig
): T & UserAgentResolvedConfig {
const normalizedAppIdProvider = normalizeProvider(input.userAgentAppId ?? DEFAULT_UA_APP_ID);
return {
...input,
customUserAgent: typeof input.customUserAgent === "string" ? [[input.customUserAgent]] : input.customUserAgent,
userAgentAppId: async () => {
const appId = await normalizedAppIdProvider();
if (!isValidUserAgentAppId(appId)) {
const logger = input.logger?.constructor?.name === 'NoOpLogger' ? console : input.logger;
if (typeof appId !== "string") {
logger?.warn("userAgentAppId must be a string or undefined.");
} else if (appId.length > 50) {
logger?.warn("The provided userAgentAppId exceeds the maximum length of 50 characters.");
}
}
return appId;
},
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("userAgentMiddleware", () => {
],
customUserAgent: [["custom_ua/abc"]],
runtime,
userAgentAppId: async () => undefined,
});
const handler = middleware(mockNextHandler, { userAgent: [["cfg/retry-mode", "standard"]] });
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });
Expand All @@ -50,6 +51,44 @@ describe("userAgentMiddleware", () => {
);
});

it("should include appId in user agent when provided", async () => {
const middleware = userAgentMiddleware({
defaultUserAgentProvider: async () => [
["default_agent", "1.0.0"],
["aws-sdk-js", "1.0.0"],
],
customUserAgent: [["custom_ua/abc"]],
runtime: "node",
userAgentAppId: async () => "test-app-id",
});
const handler = middleware(mockNextHandler, { userAgent: [["cfg/retry-mode", "standard"]] });
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });
expect(mockNextHandler.mock.calls.length).toEqual(1);
const sdkUserAgent = mockNextHandler.mock.calls[0][0].request.headers[USER_AGENT];
expect(sdkUserAgent).toEqual(expect.stringContaining("app/test-app-id"));
});

it("should include long appId in user agent when provided", async () => {
const longAppId = "a".repeat(51); // 51 characters, exceeding the 50 character limit
const middleware = userAgentMiddleware({
defaultUserAgentProvider: async () => [
["default_agent", "1.0.0"],
["aws-sdk-js", "1.0.0"],
],
customUserAgent: [["custom_ua/abc"]],
runtime: "node",
userAgentAppId: async () => longAppId,
});
const handler = middleware(mockNextHandler, {});
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });

const sdkUserAgent = mockNextHandler.mock.calls[0][0].request.headers[USER_AGENT];
expect(sdkUserAgent).toEqual(expect.stringContaining(`app/${longAppId}`));
expect(sdkUserAgent).toEqual(expect.stringContaining("aws-sdk-js/1.0.0"));
expect(sdkUserAgent).toEqual(expect.stringContaining("default_agent/1.0.0"));
expect(sdkUserAgent).toEqual(expect.stringContaining("custom_ua/abc"));
});

describe("should sanitize the SDK user agent string", () => {
const cases: { ua: UserAgentPair; expected: string }[] = [
{ ua: ["/name", "1.0.0"], expected: "name/1.0.0" },
Expand All @@ -72,6 +111,7 @@ describe("userAgentMiddleware", () => {
const middleware = userAgentMiddleware({
defaultUserAgentProvider: async () => [ua],
runtime,
userAgentAppId: async () => undefined,
});
const handler = middleware(mockNextHandler, {});
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });
Expand All @@ -84,6 +124,7 @@ describe("userAgentMiddleware", () => {
const middleware = userAgentMiddleware({
defaultUserAgentProvider: async () => [ua],
runtime,
userAgentAppId: async () => undefined,
});

// internal variant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@ export const userAgentMiddleware =
if (!HttpRequest.isInstance(request)) return next(args);
const { headers } = request;
const userAgent = context?.userAgent?.map(escapeUserAgent) || [];
const defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent);
let defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent);
const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || [];
const appId = await options.userAgentAppId();
if (appId) {
defaultUserAgent.push(escapeUserAgent([`app/${appId}`]));
}
const prefix = getUserAgentPrefix();

// Set value to AWS-specific user agent header
Expand Down
65 changes: 46 additions & 19 deletions packages/util-user-agent-browser/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
import { defaultUserAgent } from ".";
import { defaultUserAgent, PreviouslyResolved } from ".";

const ua =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36";

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[2]).toEqual(["os/macOS", "10.15.7"]);
expect(userAgent[3]).toEqual(["lang/js"]);
expect(userAgent[4]).toEqual(["md/browser", "Chrome_86.0.4240.111"]);
expect(userAgent[5]).toEqual(["api/s3", "0.1.0"]);
jest.clearAllMocks();
});

it("should populate metrics when service id not available", async () => {
jest.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua);
const userAgent = await defaultUserAgent({ serviceId: undefined, clientVersion: "0.1.0" })();
expect(userAgent).not.toContainEqual(["api/s3", "0.1.0"]);
jest.clearAllMocks();
});
const mockConfig: PreviouslyResolved = {
userAgentAppId: jest.fn().mockResolvedValue(undefined),
};

describe("defaultUserAgent", () => {
beforeEach(() => {
jest.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua);
});

afterEach(() => {
jest.clearAllMocks();
});

it("should populate metrics", async () => {
const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })(mockConfig);
expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]);
expect(userAgent[1]).toEqual(["ua", "2.0"]);
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"]);
expect(userAgent[5]).toEqual(["api/s3", "0.1.0"]);
expect(userAgent.length).toBe(6);
});

it("should populate metrics when service id not available", async () => {
const userAgent = await defaultUserAgent({ serviceId: undefined, clientVersion: "0.1.0" })(mockConfig);
expect(userAgent).not.toContainEqual(["api/s3", "0.1.0"]);
expect(userAgent.length).toBe(5);
});

it("should include appId when provided", async () => {
const configWithAppId: PreviouslyResolved = {
userAgentAppId: jest.fn().mockResolvedValue("test-app-id"),
};
const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })(configWithAppId);
expect(userAgent[6]).toEqual(["app/test-app-id"]);
expect(userAgent.length).toBe(7);
});

it("should not include appId when not provided", async () => {
const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })(mockConfig);
expect(userAgent).not.toContainEqual(expect.arrayContaining(["app/"]));
expect(userAgent.length).toBe(6);
});
});
13 changes: 11 additions & 2 deletions packages/util-user-agent-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ import bowser from "bowser";

import { DefaultUserAgentOptions } from "./configurations";

export interface PreviouslyResolved {
userAgentAppId: Provider<string | undefined>;
}

/**
* @internal
*
* Default provider to the user agent in browsers. It's a best effort to infer
* the device information. It uses bowser library to detect the browser and version
*/
export const defaultUserAgent =
({ serviceId, clientVersion }: DefaultUserAgentOptions): Provider<UserAgent> =>
async () => {
({ serviceId, clientVersion }: DefaultUserAgentOptions): ((config: PreviouslyResolved) => Promise<UserAgent>) =>
async (config?: PreviouslyResolved) => {
const parsedUA =
typeof window !== "undefined" && window?.navigator?.userAgent
? bowser.parse(window.navigator.userAgent)
Expand All @@ -36,5 +40,10 @@ export const defaultUserAgent =
sections.push([`api/${serviceId}`, clientVersion]);
}

const appId = await config?.userAgentAppId?.();
if (appId) {
sections.push([`app/${appId}`]);
}

return sections;
};
1 change: 1 addition & 0 deletions packages/util-user-agent-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/middleware-user-agent": "*",
"@aws-sdk/types": "*",
"@smithy/node-config-provider": "^3.1.7",
"@smithy/types": "^3.4.2",
Expand Down
Loading
Loading