Skip to content

Commit 9d72ddc

Browse files
committed
chore(util-user-agent-bbrowser): remove bowser from default UA provider
1 parent 8f16327 commit 9d72ddc

File tree

8 files changed

+290
-75
lines changed

8 files changed

+290
-75
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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 module = await import("bowser");
20+
const parse = module.parse ?? module.default.parse ?? (() => "");
21+
22+
const parsedUA =
23+
typeof window !== "undefined" && window?.navigator?.userAgent ? parse(window.navigator.userAgent) : undefined;
24+
const sections: UserAgent = [
25+
// sdk-metadata
26+
["aws-sdk-js", clientVersion],
27+
// ua-metadata
28+
["ua", "2.1"],
29+
// os-metadata
30+
[`os/${parsedUA?.os?.name || "other"}`, parsedUA?.os?.version],
31+
// language-metadata
32+
// ECMAScript edition doesn't matter in JS.
33+
["lang/js"],
34+
// browser vendor and version.
35+
["md/browser", `${parsedUA?.browser?.name ?? "unknown"}_${parsedUA?.browser?.version ?? "unknown"}`],
36+
];
37+
38+
if (serviceId) {
39+
// api-metadata
40+
// service Id may not appear in non-AWS clients
41+
sections.push([`api/${serviceId}`, clientVersion]);
42+
}
43+
44+
const appId = await config?.userAgentAppId?.();
45+
if (appId) {
46+
sections.push([`app/${appId}`]);
47+
}
48+
49+
return sections;
50+
};

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: 81 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,95 @@
1-
import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest";
1+
import { describe, expect, test as it } from "vitest";
22

3-
import { createDefaultUserAgentProvider, PreviouslyResolved } from ".";
3+
import { createDefaultUserAgentProvider, fallback } from "./index";
44

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),
5+
type NavigatorTestAugment = {
6+
userAgentData?: {
7+
brands?: {
8+
brand?: string;
9+
version?: string;
10+
}[];
11+
platform?: string;
12+
};
1013
};
1114

12-
describe("createDefaultUserAgentProvider", () => {
13-
beforeEach(() => {
14-
vi.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua);
15-
});
16-
17-
afterEach(() => {
18-
vi.clearAllMocks();
19-
});
15+
describe("browser and os brand detection", () => {
16+
it("should use navigator.userAgentData when available", async () => {
17+
const navigator = window.navigator as typeof window.navigator & NavigatorTestAugment;
18+
navigator.userAgentData = {
19+
platform: "linux",
20+
brands: [
21+
{ brand: "test", version: "1" },
22+
{ brand: "BROWSER_BRAND", version: "10000" },
23+
],
24+
};
2025

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-
});
26+
const uaProvider = createDefaultUserAgentProvider({
27+
serviceId: "AWS",
28+
clientVersion: "3.0.0",
29+
});
3130

32-
it("should populate metrics when service id not available", async () => {
33-
const userAgent = await createDefaultUserAgentProvider({ serviceId: undefined, clientVersion: "0.1.0" })(
34-
mockConfig
31+
const sdkUa = await uaProvider();
32+
expect(sdkUa.flatMap((_) => _.filter(Boolean).join("#")).join(" ")).toEqual(
33+
"aws-sdk-js#3.0.0 ua#2.1 os/linux lang/js md/browser#BROWSER_BRAND_10000 api/AWS#3.0.0"
3534
);
36-
expect(userAgent).not.toContainEqual(["api/s3", "0.1.0"]);
37-
expect(userAgent.length).toBe(5);
3835
});
3936

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
37+
it("uses defaults when unable to detect any specific OS or browser brand", async () => {
38+
const navigator = window.navigator as typeof window.navigator & NavigatorTestAugment;
39+
delete navigator.userAgentData;
40+
41+
const uaProvider = createDefaultUserAgentProvider({
42+
serviceId: "AWS",
43+
clientVersion: "3.0.0",
44+
});
45+
46+
const sdkUa = await uaProvider();
47+
expect(sdkUa.flatMap((_) => _.filter(Boolean).join("#")).join(" ")).toEqual(
48+
"aws-sdk-js#3.0.0 ua#2.1 os/other lang/js md/browser#unknown_unknown api/AWS#3.0.0"
4649
);
47-
expect(userAgent[6]).toEqual(["app/test-app-id"]);
48-
expect(userAgent.length).toBe(7);
4950
});
51+
});
52+
53+
describe("ua fallback parsing", () => {
54+
const samples = [
55+
`Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15`,
56+
`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`,
57+
`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`,
58+
`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`,
59+
`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36`,
60+
`Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36`,
61+
`Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.44 Mobile Safari/537.36`,
62+
`Mozilla/5.0 (Linux; Android 16; LM-Q710(FGN)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.44 Mobile Safari/537.36`,
63+
`Mozilla/5.0 (Android 16; Mobile; LG-M255; rv:143.0) Gecko/143.0 Firefox/143.0`,
64+
`Mozilla/5.0 (Android 16; Mobile; rv:68.0) Gecko/68.0 Firefox/143.0`,
65+
`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`,
66+
`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`,
67+
`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`,
68+
`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`,
69+
`Mozilla/5.0 (X11; Linux i686; rv:143.0) Gecko/20100101 Firefox/143.0`,
70+
];
71+
72+
const expectedBrands = [
73+
["macOS", "Safari"],
74+
["iOS", "Safari"],
75+
["iOS", "Safari"],
76+
["macOS", "Chrome"],
77+
["Windows", "Chrome"],
78+
["Linux", "Chrome"],
79+
["Android", "Chrome"],
80+
["Android", "Chrome"],
81+
["Android", "Firefox"],
82+
["Android", "Firefox"],
83+
["iOS", "Microsoft Edge"],
84+
["Android", "Microsoft Edge"],
85+
["macOS", "Microsoft Edge"],
86+
["Windows", "Microsoft Edge"],
87+
["Linux", "Firefox"],
88+
];
5089

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);
90+
it("should detect os and browser", () => {
91+
for (let i = 0; i < expectedBrands.length; ++i) {
92+
expect([fallback.os(samples[i]), fallback.browser(samples[i])]).toEqual(expectedBrands[i]);
93+
}
5594
});
5695
});

0 commit comments

Comments
 (0)