Skip to content

Commit 5523047

Browse files
committed
chore(util-user-agent-bbrowser): remove bowser from default UA provider
1 parent 7642359 commit 5523047

File tree

8 files changed

+255
-87
lines changed

8 files changed

+255
-87
lines changed

packages/util-user-agent-browser/README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,27 @@
33
[![NPM version](https://img.shields.io/npm/v/@aws-sdk/util-user-agent-browser/latest.svg)](https://www.npmjs.com/package/@aws-sdk/util-user-agent-browser)
44
[![NPM downloads](https://img.shields.io/npm/dm/@aws-sdk/util-user-agent-browser.svg)](https://www.npmjs.com/package/@aws-sdk/util-user-agent-browser)
55

6-
> An internal package
7-
86
## Usage
97

10-
You probably shouldn't, at least directly.
8+
In previous versions of the AWS SDK for JavaScript v3, the AWS SDK user agent header was provided by parsing the navigator user agent string with the `bowser` library.
9+
10+
This was later changed to browser feature detection using the native Navigator APIs, but if you would like to have the previous functionality, use the following code:
11+
12+
```js
13+
import { createUserAgentStringParsingProvider } from "@aws-sdk/util-user-agent-browser";
14+
15+
import { S3Client } from "@aws-sdk/client-s3";
16+
import pkgInfo from "@aws-sdk/client-s3/package.json";
17+
// or any other client.
18+
19+
const client = new S3Client({
20+
defaultUserAgentProvider: createUserAgentStringParsingProvider({
21+
// For a client's serviceId, check the corresponding shared runtimeConfig file
22+
// https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/src/runtimeConfig.shared.ts
23+
serviceId: "S3",
24+
clientVersion: pkgInfo.version,
25+
}),
26+
});
27+
```
28+
29+
This usage is not recommended, due to the size of the additional parsing library.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest";
2+
3+
import { createUserAgentStringParsingProvider } from "./createUserAgentStringParsingProvider";
4+
import type { PreviouslyResolved } from "./index";
5+
6+
const ua =
7+
"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";
8+
9+
const mockConfig: PreviouslyResolved = {
10+
userAgentAppId: vi.fn().mockResolvedValue(undefined),
11+
};
12+
13+
describe("createUserAgentStringParsingProvider", () => {
14+
beforeEach(() => {
15+
vi.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua);
16+
});
17+
18+
afterEach(() => {
19+
vi.clearAllMocks();
20+
});
21+
22+
it("should populate metrics", async () => {
23+
const userAgent = await createUserAgentStringParsingProvider({ serviceId: "s3", clientVersion: "0.1.0" })(
24+
mockConfig
25+
);
26+
expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]);
27+
expect(userAgent[1]).toEqual(["ua", "2.1"]);
28+
expect(userAgent[2]).toEqual(["os/macOS", "10.15.7"]);
29+
expect(userAgent[3]).toEqual(["lang/js"]);
30+
expect(userAgent[4]).toEqual(["md/browser", "Chrome_86.0.4240.111"]);
31+
expect(userAgent[5]).toEqual(["api/s3", "0.1.0"]);
32+
expect(userAgent.length).toBe(6);
33+
});
34+
35+
it("should populate metrics when service id not available", async () => {
36+
const userAgent = await createUserAgentStringParsingProvider({ serviceId: undefined, clientVersion: "0.1.0" })(
37+
mockConfig
38+
);
39+
expect(userAgent).not.toContainEqual(["api/s3", "0.1.0"]);
40+
expect(userAgent.length).toBe(5);
41+
});
42+
43+
it("should include appId when provided", async () => {
44+
const configWithAppId: PreviouslyResolved = {
45+
userAgentAppId: vi.fn().mockResolvedValue("test-app-id"),
46+
};
47+
const userAgent = await createUserAgentStringParsingProvider({ serviceId: "s3", clientVersion: "0.1.0" })(
48+
configWithAppId
49+
);
50+
expect(userAgent[6]).toEqual(["app/test-app-id"]);
51+
expect(userAgent.length).toBe(7);
52+
});
53+
54+
it("should not include appId when not provided", async () => {
55+
const userAgent = await createUserAgentStringParsingProvider({ serviceId: "s3", clientVersion: "0.1.0" })(
56+
mockConfig
57+
);
58+
expect(userAgent).not.toContainEqual(expect.arrayContaining(["app/"]));
59+
expect(userAgent.length).toBe(6);
60+
});
61+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { UserAgent } from "@smithy/types";
2+
3+
import type { DefaultUserAgentOptions } from "./configurations";
4+
import type { PreviouslyResolved } from "./index";
5+
6+
/**
7+
* This is an alternative to the default user agent provider that uses the bowser
8+
* library to parse the user agent string.
9+
*
10+
* Use this with your client's `defaultUserAgentProvider` constructor object field
11+
* to use the legacy behavior.
12+
*
13+
* @deprecated use the default provider unless you need the older UA-parsing functionality.
14+
* @public
15+
*/
16+
export const createUserAgentStringParsingProvider =
17+
({ serviceId, clientVersion }: DefaultUserAgentOptions): ((config?: PreviouslyResolved) => Promise<UserAgent>) =>
18+
async (config?: PreviouslyResolved) => {
19+
const bowser = await import("bowser");
20+
21+
const parsedUA =
22+
typeof window !== "undefined" && window?.navigator?.userAgent
23+
? bowser.parse(window.navigator.userAgent)
24+
: undefined;
25+
const sections: UserAgent = [
26+
// sdk-metadata
27+
["aws-sdk-js", clientVersion],
28+
// ua-metadata
29+
["ua", "2.1"],
30+
// os-metadata
31+
[`os/${parsedUA?.os?.name || "other"}`, parsedUA?.os?.version],
32+
// language-metadata
33+
// ECMAScript edition doesn't matter in JS.
34+
["lang/js"],
35+
// browser vendor and version.
36+
["md/browser", `${parsedUA?.browser?.name ?? "unknown"}_${parsedUA?.browser?.version ?? "unknown"}`],
37+
];
38+
39+
if (serviceId) {
40+
// api-metadata
41+
// service Id may not appear in non-AWS clients
42+
sections.push([`api/${serviceId}`, clientVersion]);
43+
}
44+
45+
const appId = await config?.userAgentAppId?.();
46+
if (appId) {
47+
sections.push([`app/${appId}`]);
48+
}
49+
50+
return sections;
51+
};

packages/util-user-agent-browser/src/index.native.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import { Provider, UserAgent } from "@smithy/types";
22

33
import { DefaultUserAgentOptions } from "./configurations";
44

5+
/**
6+
* @internal
7+
*/
58
export interface PreviouslyResolved {
69
userAgentAppId: Provider<string | undefined>;
710
}
811

912
/**
13+
* Default provider to the user agent in ReactNative.
1014
* @internal
11-
*
12-
* Default provider to the user agent in ReactNative. It's a best effort to infer
13-
* the device information. It uses bowser library to detect the browser and virsion
1415
*/
1516
export const createDefaultUserAgentProvider =
1617
({ serviceId, clientVersion }: DefaultUserAgentOptions): ((config?: PreviouslyResolved) => Promise<UserAgent>) =>
Lines changed: 45 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,47 @@
1-
import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest";
2-
3-
import { createDefaultUserAgentProvider, PreviouslyResolved } from ".";
4-
5-
const ua =
6-
"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";
7-
8-
const mockConfig: PreviouslyResolved = {
9-
userAgentAppId: vi.fn().mockResolvedValue(undefined),
10-
};
11-
12-
describe("createDefaultUserAgentProvider", () => {
13-
beforeEach(() => {
14-
vi.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua);
15-
});
16-
17-
afterEach(() => {
18-
vi.clearAllMocks();
19-
});
20-
21-
it("should populate metrics", async () => {
22-
const userAgent = await createDefaultUserAgentProvider({ serviceId: "s3", clientVersion: "0.1.0" })(mockConfig);
23-
expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]);
24-
expect(userAgent[1]).toEqual(["ua", "2.1"]);
25-
expect(userAgent[2]).toEqual(["os/macOS", "10.15.7"]);
26-
expect(userAgent[3]).toEqual(["lang/js"]);
27-
expect(userAgent[4]).toEqual(["md/browser", "Chrome_86.0.4240.111"]);
28-
expect(userAgent[5]).toEqual(["api/s3", "0.1.0"]);
29-
expect(userAgent.length).toBe(6);
30-
});
31-
32-
it("should populate metrics when service id not available", async () => {
33-
const userAgent = await createDefaultUserAgentProvider({ serviceId: undefined, clientVersion: "0.1.0" })(
34-
mockConfig
35-
);
36-
expect(userAgent).not.toContainEqual(["api/s3", "0.1.0"]);
37-
expect(userAgent.length).toBe(5);
38-
});
39-
40-
it("should include appId when provided", async () => {
41-
const configWithAppId: PreviouslyResolved = {
42-
userAgentAppId: vi.fn().mockResolvedValue("test-app-id"),
43-
};
44-
const userAgent = await createDefaultUserAgentProvider({ serviceId: "s3", clientVersion: "0.1.0" })(
45-
configWithAppId
46-
);
47-
expect(userAgent[6]).toEqual(["app/test-app-id"]);
48-
expect(userAgent.length).toBe(7);
49-
});
50-
51-
it("should not include appId when not provided", async () => {
52-
const userAgent = await createDefaultUserAgentProvider({ serviceId: "s3", clientVersion: "0.1.0" })(mockConfig);
53-
expect(userAgent).not.toContainEqual(expect.arrayContaining(["app/"]));
54-
expect(userAgent.length).toBe(6);
1+
import { describe, expect, test as it } from "vitest";
2+
3+
import { fallback } from "./index";
4+
5+
describe("ua fallback parsing", () => {
6+
const samples = [
7+
`Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15`,
8+
`Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6.2 Mobile/15E148 Safari/604.1`,
9+
`Mozilla/5.0 (iPad; CPU OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6.2 Mobile/15E148 Safari/604.1`,
10+
`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36`,
11+
`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36`,
12+
`Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36`,
13+
`Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.44 Mobile Safari/537.36`,
14+
`Mozilla/5.0 (Linux; Android 16; LM-Q710(FGN)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.44 Mobile Safari/537.36`,
15+
`Mozilla/5.0 (Android 16; Mobile; LG-M255; rv:143.0) Gecko/143.0 Firefox/143.0`,
16+
`Mozilla/5.0 (Android 16; Mobile; rv:68.0) Gecko/68.0 Firefox/143.0`,
17+
`Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 EdgiOS/140.3485.94 Mobile/15E148 Safari/605.1.15`,
18+
`Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.44 Mobile Safari/537.36 EdgA/140.0.3485.98`,
19+
`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.3537.57`,
20+
`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.3537.57`,
21+
`Mozilla/5.0 (X11; Linux i686; rv:143.0) Gecko/20100101 Firefox/143.0`,
22+
];
23+
24+
const expectedBrands = [
25+
["macOS", "Safari"],
26+
["iOS", "Safari"],
27+
["iOS", "Safari"],
28+
["macOS", "Chrome"],
29+
["Windows", "Chrome"],
30+
["Linux", "Chrome"],
31+
["Android", "Chrome"],
32+
["Android", "Chrome"],
33+
["Android", "Firefox"],
34+
["Android", "Firefox"],
35+
["iOS", "Microsoft Edge"],
36+
["Android", "Microsoft Edge"],
37+
["macOS", "Microsoft Edge"],
38+
["Windows", "Microsoft Edge"],
39+
["Linux", "Firefox"],
40+
];
41+
42+
it("should detect os and browser", () => {
43+
for (let i = 0; i < expectedBrands.length; ++i) {
44+
expect([fallback.os(samples[i]), fallback.browser(samples[i])]).toEqual(expectedBrands[i]);
45+
}
5546
});
5647
});

packages/util-user-agent-browser/src/index.ts

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,94 @@
11
import { Provider, UserAgent } from "@smithy/types";
2-
import bowser from "bowser";
32

4-
import { DefaultUserAgentOptions } from "./configurations";
3+
import type { DefaultUserAgentOptions } from "./configurations";
54

5+
export { createUserAgentStringParsingProvider } from "./createUserAgentStringParsingProvider";
6+
7+
/**
8+
* @internal
9+
*/
610
export interface PreviouslyResolved {
711
userAgentAppId: Provider<string | undefined>;
812
}
913

1014
/**
1115
* @internal
12-
*
13-
* Default provider to the user agent in browsers. It's a best effort to infer
14-
* the device information. It uses bowser library to detect the browser and version
16+
*/
17+
type NavigatorAugment = {
18+
userAgentData?: {
19+
brands?: {
20+
brand?: string;
21+
version?: string;
22+
}[];
23+
platform?: string;
24+
};
25+
};
26+
27+
/**
28+
* Default provider of the AWS SDK user agent string in react-native.
29+
* @internal
1530
*/
1631
export const createDefaultUserAgentProvider =
1732
({ serviceId, clientVersion }: DefaultUserAgentOptions): ((config?: PreviouslyResolved) => Promise<UserAgent>) =>
1833
async (config?: PreviouslyResolved) => {
19-
const parsedUA =
20-
typeof window !== "undefined" && window?.navigator?.userAgent
21-
? bowser.parse(window.navigator.userAgent)
22-
: undefined;
34+
const navigator =
35+
typeof window !== "undefined" ? (window.navigator as typeof window.navigator & NavigatorAugment) : undefined;
36+
37+
const uaString = navigator?.userAgent ?? "";
38+
39+
// Sample values: macOS, iOS, Windows, Android, Linux
40+
const osName = navigator?.userAgentData?.platform ?? fallback.os(uaString) ?? "other";
41+
// We're not going to attempt to detect the OS version in a browser.
42+
const osVersion = undefined;
43+
44+
const brands = navigator?.userAgentData?.brands ?? [];
45+
const brand = brands[brands.length - 1];
46+
47+
// Sample values: Safari, Chrome, Firefox, Microsoft Edge
48+
const browserName = brand?.brand ?? fallback.browser(uaString) ?? "unknown";
49+
const browserVersion = brand?.version ?? "unknown";
50+
2351
const sections: UserAgent = [
24-
// sdk-metadata
2552
["aws-sdk-js", clientVersion],
26-
// ua-metadata
2753
["ua", "2.1"],
28-
// os-metadata
29-
[`os/${parsedUA?.os?.name || "other"}`, parsedUA?.os?.version],
30-
// language-metadata
31-
// ECMAScript edition doesn't matter in JS.
54+
[`os/${osName}`, osVersion],
3255
["lang/js"],
33-
// browser vendor and version.
34-
["md/browser", `${parsedUA?.browser?.name ?? "unknown"}_${parsedUA?.browser?.version ?? "unknown"}`],
56+
["md/browser", `${browserName}_${browserVersion}`],
3557
];
3658

3759
if (serviceId) {
38-
// api-metadata
39-
// service Id may not appear in non-AWS clients
4060
sections.push([`api/${serviceId}`, clientVersion]);
4161
}
4262

4363
const appId = await config?.userAgentAppId?.();
4464
if (appId) {
4565
sections.push([`app/${appId}`]);
4666
}
47-
4867
return sections;
4968
};
5069

70+
/**
71+
* Rudimentary UA string parsing as a fallback.
72+
* @internal
73+
*/
74+
export const fallback = {
75+
os(ua: string): string | undefined {
76+
if (/iPhone|iPad|iPod/.test(ua)) return "iOS";
77+
if (/Macintosh|Mac OS X/.test(ua)) return "macOS";
78+
if (/Windows NT/.test(ua)) return "Windows";
79+
if (/Android/.test(ua)) return "Android";
80+
if (/Linux/.test(ua)) return "Linux";
81+
return undefined;
82+
},
83+
browser(ua: string): string | undefined {
84+
if (/EdgiOS|EdgA|Edg\//.test(ua)) return "Microsoft Edge";
85+
if (/Firefox\//.test(ua)) return "Firefox";
86+
if (/Chrome\//.test(ua)) return "Chrome";
87+
if (/Safari\//.test(ua) && !/Chrome\//.test(ua)) return "Safari";
88+
return undefined;
89+
},
90+
};
91+
5192
/**
5293
* @internal
5394
* @deprecated use createDefaultUserAgentProvider

packages/util-user-agent-node/src/crt-availability.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/**
2-
* @internal
3-
*
42
* If \@aws-sdk/signature-v4-crt is installed and loaded, it will register
53
* this value to true.
4+
* @internal
65
*/
76
export const crtAvailability = {
87
isCrtAvailable: false,

0 commit comments

Comments
 (0)